Skip to content

@frapx/react-canvas

react-* — framework binding Wraps @frapx/fx-canvas for React.

A thin lifecycle adapter around @frapx/fx-canvas — it does not replace the core render loop with React state, and it does not ship any CSS.

Terminal window
pnpm add @frapx/fx-canvas @frapx/react-canvas
import { ShaderBackground } from "@frapx/react-canvas";
import { glsl } from "@frapx/fx-canvas";
const fragment = glsl`
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
gl_FragColor = vec4(uv, 0.5 + 0.5 * sin(u_time), 1.0);
}
`;
export function HeroBackground() {
return <ShaderBackground fragment={fragment} className="hero-bg" />;
}

The component renders a wrapper div (position: relative, overflow: hidden by default) and mounts the shader canvas into it. Most createShaderBackground options — including feedback — pass straight through as props, and changing them recreates the underlying instance. They are compared by value (plain objects and arrays by contents, functions and elements by reference), so an inline feedback={{ format: "rgba16f" }} keeps the instance while an inline dpr={() => 1} recreates it; recreateKey forces a new one. uniforms and textures update the existing instance instead of recreating it, and a new fragment is swapped in place with setFragment(), so edits and Fast Refresh keep the time and feedback. A compile error goes to error / onShaderError while the previous shader keeps drawing.

import { useMemo } from "react";
import { useShaderBackground } from "@frapx/react-canvas";
export function ShaderPanel({ intensity }: { intensity: number }) {
const uniforms = useMemo(() => ({ intensity }), [intensity]);
const { ref, instance, status, error } = useShaderBackground({
fragment,
uniforms,
feedback: true
});
return <div ref={ref} />;
}

The hook recreates the instance on the same value comparison, so feedback, vertex and the like need no dependency array. The optional second argument lists extra values whose change rebuilds the instance. status follows the instance live ("paused" offscreen or hidden, "context-lost", …) through onStatusChange, and an onStatusChange option is still called.

import { useRef } from "react";
import { ShaderBackground, type ShaderBackgroundHandle } from "@frapx/react-canvas";
const ref = useRef<ShaderBackgroundHandle>(null);
<ShaderBackground ref={ref} fragment={fragment} />;
ref.current?.stop();
ref.current?.setUniform("intensity", 1);

clock and resolution are props like any other option, but changing them switches the existing instance in place instead of recreating it. The handle exposes setTime, setClock, setTimeSource, setResolution, setFragment, captureFrame and resetFeedback:

<ShaderBackground ref={ref} fragment={fragment} clock="manual" resolution={[1920, 1080]} />;
ref.current?.setTime(2);
const png = await ref.current?.captureFrame();

timeSource follows an external transport such as an audio player while the offscreen, hidden and reduced-motion gates keep working (see Following an audio transport). The latest function is called on every frame, so an inline arrow function neither recreates the instance nor restarts u_delta:

<ShaderBackground fragment={fragment} timeSource={() => transport.getPositionMs() / 1000} />

uniformSources binds sources such as keyframes() in array order (a later one wins for the same uniform). Sources are compared by identity, so an inline array of the same sources does not rebind, and a recreated instance is bound again. ref.current?.bindUniforms(source) binds one more on the current instance and returns its removal.

import { keyframes } from "@frapx/fx-canvas";
const timeline = keyframes({ glow: [[0, 0], [2, 1]] });
<ShaderBackground fragment={fragment} uniformSources={[timeline]} />;
  • React 18+ is supported.
  • Safe to import during SSR — WebGL work starts only after mount.
  • StrictMode double-mounting is handled by destroying the previous instance in effect cleanup.
  • Use onShaderReady / onShaderError instead of core onReady / onError. The component also accepts the wrapper div’s props, and onError is the div’s DOM error event. The hook uses the same names.

The wrapper div is aria-hidden="true" by default, since a background effect is decoration. When the canvas carries meaning, describe it and override the default:

<ShaderBackground
fragment={fragment}
role="img"
aria-label="Cyan rings pulsing outward on a dark background"
aria-hidden={false}
/>

With the hook, put the same attributes on the element you pass ref to.

Full prop and hook reference: package README on GitHub.