Introducing v3 — a new editor, faster engine, and more.

See what's new

Hooks & Events

Shaders provides lifecycle hooks that let you respond to key moments in the rendering pipeline. This is useful for orchestrating UI transitions, hiding loading states, or triggering animations once the shader is ready.

onReady

The onReady hook fires once after the GPU has compiled the shader and the first frame is ready to render. This is the ideal moment to fade in the shader, remove a loading skeleton, or start a dependent animation.

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

Timing

onReady fires after the shader's node tree has been compiled into a GPU pipeline and is actively rendering. This means:

  • The WebGPU renderer is initialized
  • All child shader components have registered
  • The composed shader pipeline has been compiled
  • The first frame is ready to display

The callback fires once per shader lifecycle. If the component is unmounted and remounted, it will fire again after recompilation.

Notes

  • onReady is called asynchronously (via microtask) so the rendered frame is available by the time your callback executes
  • If the <Shader> component starts off-screen and initializes lazily when scrolled into view, onReady fires after that deferred initialization completes

Unsupported browsers

Shaders is WebGPU-only. On a browser or driver that can't provide a working GPU device — an older Safari, Firefox with WebGPU still behind a flag, a blocklisted or exhausted driver — the canvas simply stays transparent. onReady never fires, nothing is written to the console, and the rest of your page is completely unaffected.

If you want to render your own fallback instead of empty space, you have two options.

Check before you mount

isWebGPUSupported() is a synchronous check for whether the browser exposes WebGPU at all. getWebGPUSupport() goes further and actually asks for an adapter, so it also catches blocklisted drivers — its result is cached for the page.

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

React to it after the fact

The unavailable event fires at most once if the GPU can't be used — either because it was never available, or because it 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 has already been cleared and every GPU resource released.

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

Debugging

Shaders keeps quiet in production on purpose — an embedded background effect must never fill a host page's console with errors the visitor can do nothing about. When you need to see what the renderer is actually doing, switch diagnostics on:

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