---
title: Hooks and events
description: Run code when an effect is ready to show, and show a fallback when the GPU can't run it.
icon: bell
category: code
---

# Hooks and events

Run code when an effect is ready to show, and show a fallback when the GPU can't run it.

## Fading in when the effect is ready

Before the first frame, Shaders turns your components into a program the GPU runs. This step is **compiling**. `onReady` fires once compiling is done and the first frame is ready to show.

Start the `<Shader>` hidden and fade it in from `onReady`:

```vue-html
<template>
  <Shader @ready="onShaderReady" :style="{ opacity: visible ? 1 : 0, transition: 'opacity 0.5s' }">
    <Circle color="#ff0088" />
  </Shader>
</template>

<script setup>
import { ref } from 'vue'

const visible = ref(false)

function onShaderReady() {
  visible.value = true
}
</script>
```

```jsx
import { useState } from 'react'

function App() {
  const [visible, setVisible] = useState(false)

  return (
    <Shader
      onReady={() => setVisible(true)}
      style={{ opacity: visible ? 1 : 0, transition: 'opacity 0.5s' }}
    >
      <Circle color="#ff0088" />
    </Shader>
  )
}
```

```tsx
<script>
  let visible = $state(false)
</script>

<Shader onready={() => visible = true} style:opacity={visible ? 1 : 0} style:transition="opacity 0.5s">
  <Circle color="#ff0088" />
</Shader>
```

```tsx
import { createSignal } from 'solid-js'

function App() {
  const [visible, setVisible] = createSignal(false)

  return (
    <Shader
      onReady={() => setVisible(true)}
      style={{ opacity: visible() ? 1 : 0, transition: 'opacity 0.5s' }}
    >
      <Circle color="#ff0088" />
    </Shader>
  )
}
```

```js
const canvas = document.getElementById('my-canvas')

// Start hidden
canvas.style.opacity = '0'
canvas.style.transition = 'opacity 0.5s'

const shader = await createShader(canvas, {
  components: [
    { type: 'Circle', id: 'c1', props: { color: '#ff0088' } }
  ]
}, {
  onReady: () => {
    canvas.style.opacity = '1'
  }
})
```

The same moment works for removing a loading skeleton or starting a dependent animation.

### When `onReady` fires

By the time `onReady` runs:

- **The renderer**: WebGPU is initialized.
- **The components**: every child inside the `<Shader>` has registered.
- **The program**: the components have been compiled into one GPU pipeline.
- **The first frame**: it's ready to display.

`onReady` fires once per mount. Unmount and remount the component and it fires again after the next compile.

The callback runs in a microtask, so the rendered frame is available when your code runs. A `<Shader>` that starts offscreen initializes when it scrolls into view, and `onReady` fires after that deferred start.

## Handling browsers without WebGPU

Shaders runs on WebGPU only. Some browsers or drivers can't provide a working GPU device: an older Safari, Firefox with WebGPU still behind a flag, a blocklisted or exhausted driver. There the canvas stays transparent, `onReady` never fires, and nothing is written to the console. The rest of your page is unaffected.

To show your own fallback instead of empty space, check before you mount or react after the fact.

### Checking support before you mount

`isWebGPUSupported()` is a synchronous check for whether the browser exposes WebGPU at all. `getWebGPUSupport()` also requests an adapter, so it catches blocklisted drivers too. Its result is cached for the page.

Mount the `<Shader>` only when `getWebGPUSupport()` reports support:

```vue-html
<template>
  <Shader v-if="supported">
    <Circle color="#ff0088" />
  </Shader>
  <div v-else class="fallback" />
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { Shader, Circle, getWebGPUSupport } from 'shaders/vue'

const supported = ref(false)
onMounted(async () => {
  supported.value = (await getWebGPUSupport()).supported
})
</script>
```

```jsx
import { useState, useEffect } from 'react'
import { Shader, Circle, getWebGPUSupport } from 'shaders/react'

function App() {
  const [supported, setSupported] = useState(false)
  useEffect(() => {
    getWebGPUSupport().then(({ supported }) => setSupported(supported))
  }, [])

  return supported
    ? <Shader><Circle color="#ff0088" /></Shader>
    : <div className="fallback" />
}
```

```tsx
<script>
  import { Shader, Circle, getWebGPUSupport } from 'shaders/svelte'

  let supported = $state(false)
  getWebGPUSupport().then((info) => { supported = info.supported })
</script>

{#if supported}
  <Shader><Circle color="#ff0088" /></Shader>
{:else}
  <div class="fallback"></div>
{/if}
```

```jsx
import { createSignal, onMount, Show } from 'solid-js'
import { Shader, Circle, getWebGPUSupport } from 'shaders/solid'

function App() {
  const [supported, setSupported] = createSignal(false)
  onMount(async () => setSupported((await getWebGPUSupport()).supported))

  return (
    <Show when={supported()} fallback={<div class="fallback" />}>
      <Shader><Circle color="#ff0088" /></Shader>
    </Show>
  )
}
```

```js
import { createShader, getWebGPUSupport } from 'shaders/js'

const { supported } = await getWebGPUSupport()
if (supported) {
  await createShader(canvas, preset)
} else {
  canvas.replaceWith(fallbackElement)
}
```

### Reacting when the GPU fails

The `unavailable` event fires at most once, when the GPU can't be used. That covers a GPU that was never available and one that became unusable later: device loss with no recovery, an out-of-memory refusal, a driver that can't run the compiled pipeline. By the time it fires, the canvas is cleared and every GPU resource is released.

Listen for `unavailable` and swap in your fallback:

```vue-html
<template>
  <Shader @unavailable="showFallback = true">
    <Circle color="#ff0088" />
  </Shader>
</template>
```

```jsx
<Shader onUnavailable={(reason) => setShowFallback(true)}>
  <Circle color="#ff0088" />
</Shader>
```

```tsx
<Shader onunavailable={() => (showFallback = true)}>
  <Circle color="#ff0088" />
</Shader>
```

```jsx
<Shader onUnavailable={() => setShowFallback(true)}>
  <Circle color="#ff0088" />
</Shader>
```

```js
await createShader(canvas, preset, {
  onError: (reason) => {
    // Terminal reasons: 'unsupported' | 'no-adapter' | 'no-device' | 'init-failed'
    // | 'device-lost' | 'out-of-memory' | 'gpu-error' | 'render-failed'
    // | 'limit-exceeded' | 'unrecoverable' | 'rebuild_failed'
    showFallback(reason)
  }
})
```

In the JS package, `onError` also fires when the GPU device is lost and the instance starts rebuilding itself, so check the reason before replacing the canvas.

## Debugging a blank canvas

Shaders is silent in production by design. An embedded background effect shouldn't fill a host page's console with errors the visitor can't act on. To see what the renderer is doing, switch on diagnostics:

```js
localStorage.setItem('shaders:debug', '1')  // persists for the origin
// or, in code:
import { setShadersDebug } from 'shaders/vue'
setShadersDebug(true)
```

`setShadersDebug` is exported from every framework package and from `shaders/js`. Setting `globalThis.__SHADERS_DEBUG__ = true` also works.

## Next steps

- [Performance](/docs/guide/performance): what each layer costs and how to keep effects fast.
- [Props and reactivity](/docs/guide/props-reactivity): bind props to your framework's state and animate them.
- [Telemetry](/docs/guide/telemetry): what Shaders reports in production and how to turn it off.