shadertown

React

The ShaderCanvas component and the useShaderRenderer hook.

<ShaderCanvas />

import { ShaderCanvas } from "shadertown/react";
PropTypeDefaultNotes
shaderShaderNamenoneCatalogue slug, for example "aurora"
presetShaderConfignoneA defineShader result; other props override it
paramsShaderParams<K>registry defaultsTyped against the chosen shader
play"always" | "visible" | "hover" | "never""visible"When animation is allowed
quality{ dpr?, fps?, tier? }{ dpr: [1, 2] }Resolution ceiling, frame-rate cap, and a pinned quality tier; omit tier to follow the page's setQualityTier
onQualityChange(state) => void—The tier on screen and why, whenever it changes
startTimenumber0Clock offset, so a paused card is not stuck on frame zero
trackPointerbooleanfalseFeeds the pointer uniform; costs one listener
badgeboolean | BadgeCornerpage settingsThe credit. false removes it; a corner name moves it
fallbackReactNodenoneRendered instead of the canvas when WebGPU is missing
onStatusChange(status) => voidnonepending, ready, unsupported, error
classNamestringnoneApplied to the element wrapping the canvas, which is the same box
styleCSSPropertiesnoneApplied to the element wrapping the canvas, which is the same box
aria-hiddenbooleannoneSet it when the shader is pure decoration

Params do not remount

Changing params calls setParams on the live renderer. Dragging a slider never recompiles a pipeline or recreates a surface, so a control panel bound straight to component state is fine.

Changing shader, though, does create a new effect. Treat it as a heavier operation and give the canvas a key if you want a clean reset.

Reduced motion

ShaderCanvas reads prefers-reduced-motion and holds a still frame when the user has asked for less movement. The canvas still shows the shader; it just stops moving.

useShaderRenderer

The escape hatch, for when you want to drive the renderer from your own animation code: a scroll handler, a GSAP timeline, an XR frame loop.

const ref = useRef<HTMLCanvasElement>(null);
const renderer = useShaderRenderer(ref, myPreset);

useEffect(() => {
  const onScroll = () => renderer?.setParams({ speed: window.scrollY / 2000 });
  window.addEventListener("scroll", onScroll, { passive: true });
  return () => window.removeEventListener("scroll", onScroll);
}, [renderer]);