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:

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>
  )
}

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:

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" />
}

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:

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

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:

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: what each layer costs and how to keep effects fast.
  • Props and reactivity: bind props to your framework's state and animate them.
  • Telemetry: what Shaders reports in production and how to turn it off.