# Shader Canvas

> Minimal WebGL2 fragment-shader runner with time/resolution/pointer uniforms, DPR capping, offscreen pausing, reduced-motion still frame and CSS fallback.

- **Slug:** `shader-canvas` · **Category:** effects · **Status:** beta
- **Import:** `import { ShaderCanvas, UniformInput, UniformValue } from "@kura-ui/react/shader-canvas";`
- **Docs:** https://kuro.abstergo.fyi/components/shader-canvas
- **Registry item:** https://kuro.abstergo.fyi/r/shader-canvas.json
- **Tags:** webgl, shader, glsl, gpu, background, gradient, effect, canvas

## Install

```bash
# shadcn registry (copies source into your project)
npx shadcn@latest add https://kuro.abstergo.fyi/r/shader-canvas.json
# Kura CLI (same, plus dependency resolution and --json output)
npx @kura-ui/cli add shader-canvas
# npm package
npm install @kura-ui/react
```

## When to use

- Building any GPU visual: mesh gradients, auroras, grain, glows, orbs, generative backgrounds.
- A custom decorative shader behind a hero, card or voice surface.

## When not to use

- A ready-made effect exists — use mesh-gradient, agent-orb, grain or border-beam.
- Static gradients — plain CSS is cheaper.
- Anything that conveys information — it is aria-hidden and may fall back to a static gradient.

## Props

| Prop | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `fragmentShader` | `string` | — | yes | GLSL ES 3.00 source. Without a #version line, a header declaring u_time, u_resolution, u_pointer and `out vec4 fragColor` is prepended. |
| `uniforms` | `Record<string, UniformInput>` | — | no | Custom uniforms: number→float, [x,y]→vec2, [x,y,z]→vec3, [x,y,z,w]→vec4, or a function of time evaluated every frame. Declare them in the shader. |
| `paused` | `boolean` | `false` | no | Freeze time on the current frame. |
| `speed` | `number` | `1` | no | Time multiplier. |
| `maxDpr` | `number` | `2` | no | Device-pixel-ratio cap. |
| `pauseWhenOffscreen` | `boolean` | `true` | no | Stop rendering when not visible. |
| `staticTime` | `number` | `1.5` | no | u_time used for the reduced-motion still frame. |
| `respectReducedMotion` | `boolean` | `true` | no | Render a single still frame when the user prefers reduced motion. |
| `fallback` | `React.ReactNode` | — | no | Shown when WebGL2 is unavailable or the shader fails. Default: a soft token gradient. |
| `onError` | `(message: string) => void` | — | no | Compile/link error callback. |

Native attributes of the root element, `className` and `ref` are passed through.

## Anatomy

- **ShaderCanvas** (`data-slot="shader-canvas"`) — Container div (size it with classes); `data-state` = running | paused | static | fallback.

## Accessibility

- Decorative: the container is aria-hidden. Never put meaning only in the shader.
- Respects prefers-reduced-motion with a single still frame.
- Reduced motion: Renders one frame at `staticTime` and stops the animation loop.

## Notes for agents

- Give the container an explicit size (e.g. `className="h-64 w-full rounded-kura-xl"`); the canvas fills it.
- Animate custom uniforms with function values `(t) => …` or by mutating an object you pass — not React state per frame.
- Colors in shaders cannot read CSS variables; pass them as vec3 uniforms (linear 0–1 RGB).

## Examples

### Gradient in a few lines (minimal)

```tsx
import { ShaderCanvas } from "@kura-ui/react/shader-canvas";

const gradient = /* glsl */ `
void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;
  vec3 a = vec3(0.07, 0.44, 0.38);
  vec3 b = vec3(0.93, 0.92, 0.88);
  float t = 0.5 + 0.5 * sin(u_time * 0.6 + uv.x * 3.0 + uv.y * 2.0);
  fragColor = vec4(mix(a, b, t), 1.0);
}`;

export function ShaderCanvasMinimal() {
  return (
    <ShaderCanvas fragmentShader={gradient} className="h-48 w-full max-w-md rounded-kura-xl" />
  );
}
```

### Silk with custom uniforms (full)

Custom color uniforms, speed control, pause and pointer response.

```tsx
"use client";

import * as React from "react";
import { Button } from "@kura-ui/react/button";
import { ShaderCanvas } from "@kura-ui/react/shader-canvas";

const silk = /* glsl */ `
uniform vec3 u_colorA;
uniform vec3 u_colorB;
uniform vec3 u_colorC;

float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
float noise(vec2 p) {
  vec2 i = floor(p), f = fract(p);
  vec2 u = f * f * (3.0 - 2.0 * f);
  return mix(mix(hash(i), hash(i + vec2(1, 0)), u.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), u.x), u.y);
}
float fbm(vec2 p) {
  float v = 0.0, a = 0.5;
  for (int i = 0; i < 5; i++) { v += a * noise(p); p *= 2.02; a *= 0.5; }
  return v;
}

void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;
  vec2 p = uv * vec2(u_resolution.x / u_resolution.y, 1.0) * 2.2;
  float t = u_time * 0.12;
  vec2 q = vec2(fbm(p + t), fbm(p - t + 3.1));
  vec2 r = vec2(fbm(p + 2.0 * q + vec2(1.7, 9.2) + t), fbm(p + 2.0 * q + vec2(8.3, 2.8)));
  r += (u_pointer - 0.5) * 0.35;
  float f = fbm(p + 2.5 * r);
  vec3 col = mix(u_colorA, u_colorB, smoothstep(0.2, 0.8, f));
  col = mix(col, u_colorC, smoothstep(0.55, 1.0, length(q)));
  col += 0.04 * (hash(gl_FragCoord.xy + u_time) - 0.5); // grain
  fragColor = vec4(col, 1.0);
}`;

export function ShaderCanvasFull() {
  const [paused, setPaused] = React.useState(false);
  const [speed, setSpeed] = React.useState(1);
  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <ShaderCanvas
        fragmentShader={silk}
        uniforms={{
          u_colorA: [0.05, 0.09, 0.08],
          u_colorB: [0.12, 0.55, 0.47],
          u_colorC: [0.92, 0.9, 0.84],
        }}
        paused={paused}
        speed={speed}
        maxDpr={1.5}
        staticTime={4}
        className="h-64 w-full rounded-kura-xl shadow-kura-md"
      />
      <div className="flex items-center gap-2">
        <Button size="sm" variant="secondary" onClick={() => setPaused((p) => !p)}>
          {paused ? "Play" : "Pause"}
        </Button>
        <Button size="sm" variant="ghost" onClick={() => setSpeed((s) => (s >= 4 ? 0.5 : s * 2))}>
          Speed ×{speed}
        </Button>
      </div>
    </div>
  );
}
```

### Feature card with a live band (composition)

A ShaderCanvas header behind a glass chip, with copy, stats and Buttons on a token surface.

```tsx
import { ArrowRight, Telescope } from "lucide-react";
import { Button } from "@kura-ui/react/button";
import { ShaderCanvas } from "@kura-ui/react/shader-canvas";

const ribbons = /* glsl */ `
uniform vec3 u_deep;
uniform vec3 u_jade;
uniform vec3 u_mist;

float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }

void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;
  float aspect = u_resolution.x / u_resolution.y;
  float x = uv.x * aspect;
  float t = u_time * 0.2;

  // Deep base with a slow jade bloom that leans toward the pointer.
  vec2 bloom = vec2(0.5 + 0.25 * sin(t * 0.7), 0.35) * 0.6 + u_pointer * 0.4;
  float halo = smoothstep(0.9, 0.0, length((uv - bloom) * vec2(aspect * 0.6, 1.0)));
  vec3 col = mix(u_deep, u_jade * 0.7, halo * 0.75);

  // Three silk ribbons drifting across the band.
  float glow = 0.0;
  for (int i = 0; i < 3; i++) {
    float fi = float(i);
    float y = 0.5 + 0.2 * sin(x * (0.9 + fi * 0.3) + t * (1.0 + fi * 0.35) + fi * 2.1)
                  + 0.07 * sin(x * 2.6 - t * 1.6 + fi * 1.3);
    float d = abs(uv.y - y);
    glow += (0.012 + 0.006 * fi) / (d + 0.012) * (0.9 - fi * 0.2);
  }
  col = mix(col, u_jade, clamp(glow * 0.45, 0.0, 1.0));
  col = mix(col, u_mist, smoothstep(1.2, 3.0, glow));
  col += 0.025 * (hash(gl_FragCoord.xy + u_time) - 0.5); // grain
  fragColor = vec4(col, 1.0);
}`;

const silk = /* glsl */ `
uniform vec3 u_colorA;
uniform vec3 u_colorB;
uniform vec3 u_colorC;

float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
float noise(vec2 p) {
  vec2 i = floor(p), f = fract(p);
  vec2 u = f * f * (3.0 - 2.0 * f);
  return mix(mix(hash(i), hash(i + vec2(1, 0)), u.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), u.x), u.y);
}
float fbm(vec2 p) {
  float v = 0.0, a = 0.5;
  for (int i = 0; i < 5; i++) { v += a * noise(p); p *= 2.02; a *= 0.5; }
  return v;
}

void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;
  vec2 p = uv * vec2(u_resolution.x / u_resolution.y, 1.0) * 2.2;
  float t = u_time * 0.12;
  vec2 q = vec2(fbm(p + t), fbm(p - t + 3.1));
  vec2 r = vec2(fbm(p + 2.0 * q + vec2(1.7, 9.2) + t), fbm(p + 2.0 * q + vec2(8.3, 2.8)));
  r += (u_pointer - 0.5) * 0.35;
  float f = fbm(p + 2.5 * r);
  vec3 col = mix(u_colorA, u_colorB, smoothstep(0.2, 0.8, f));
  col = mix(col, u_colorC, smoothstep(0.55, 1.0, length(q)));
  col += 0.04 * (hash(gl_FragCoord.xy + u_time) - 0.5); // grain
  fragColor = vec4(col, 1.0);
}`;

export function ShaderCanvasFeatureCard() {
  return (
    <article className="relative isolate flex w-full max-w-md flex-col overflow-hidden rounded-kura-2xl bg-kura-surface shadow-kura-md">
      <div className="relative h-44">
        <ShaderCanvas
          fragmentShader={ribbons}
          uniforms={{
            u_deep: [0.035, 0.09, 0.08],
            u_jade: [0.12, 0.6, 0.5],
            u_mist: [0.86, 0.95, 0.9],
          }}
          maxDpr={1.5}
          staticTime={6}
          className="absolute inset-0"
        />
        <span className="absolute top-3 left-3 inline-flex items-center gap-1.5 rounded-full bg-kura-overlay px-2.5 py-1 font-medium text-kura-fg text-xs shadow-kura-xs backdrop-blur-xl">
          <Telescope className="size-3.5" aria-hidden />
          Deep research
        </span>
      </div>
      <div className="flex flex-col gap-4 p-5">
        <div className="flex flex-col gap-1.5">
          <h3 className="font-medium text-kura-fg text-lg tracking-tight">
            Hand off the reading, keep the judgement
          </h3>
          <p className="text-kura-fg-muted text-sm leading-relaxed">
            Kura searches, reads and cross-checks dozens of sources, then brings back a cited brief
            you can question line by line.
          </p>
        </div>
        <dl className="flex gap-6 font-kura-mono text-xs tabular-nums">
          <div className="flex flex-col gap-0.5">
            <dt className="text-kura-fg-subtle">Typical run</dt>
            <dd className="text-kura-fg">4–6 min</dd>
          </div>
          <div className="flex flex-col gap-0.5">
            <dt className="text-kura-fg-subtle">Sources</dt>
            <dd className="text-kura-fg">30–60</dd>
          </div>
          <div className="flex flex-col gap-0.5">
            <dt className="text-kura-fg-subtle">Included</dt>
            <dd className="text-kura-fg">25 / month</dd>
          </div>
        </dl>
        <div className="flex items-center gap-2">
          <Button>
            Start a brief
            <ArrowRight />
          </Button>
          <Button variant="ghost">See an example</Button>
        </div>
      </div>
    </article>
  );
}
```

## Dependencies

- npm: none
- Kura registry items: `kura-theme`, `kura-merge-refs`, `use-reduced-motion`, `utils`
- Related: `mesh-gradient`, `agent-orb`, `grain`, `border-beam`
