# shad-fx

Canvas effects for React, installed with the shadcn CLI. Effects (fire, lightning, sonar rings, a light beam, a sloshing fluid, rain and snow) are separate from the renderer that draws them; the first renderer is ordered dither.

This page is the complete guide for AI agents adding shad-fx to a project. The
same content, for people, is at https://www.sekei.xyz/shad-fx.

- Source: https://github.com/sekeidesign/shad-fx
- Registry namespace: `@sekei` (listed in the shadcn registry directory)

## When to use it

Reach for shad-fx when a React project wants a pixelated, dithered, retro or
8-bit animated background or accent on a card, hero, button, avatar or section:
flames, lightning, sonar or ripple rings, a spotlight beam, a liquid fill, rain
or snow. It is decoration, not content: the canvas is `aria-hidden` and
`pointer-events-none`.

It needs React and a shadcn-style project (a `components.json`, the `@/`
alias and a `cn` helper in `@/lib/utils`). There is no npm dependency: the
shadcn CLI copies the source into the project.

## Install

```bash
npx shadcn@latest add @sekei/shad-fx
npx shadcn@latest add @sekei/shad-fx-dither @sekei/shad-fx-fire
```

The first installs everything; the second, the dither renderer and one effect.
An effect alone has nothing to draw on, so always install a renderer with it.
Nothing to configure: the CLI resolves `@sekei` and writes the
`registries` entry into `components.json` itself. To pin it by hand:

```json
{
  "registries": {
    "@sekei": "https://www.sekei.xyz/registry/{name}.json"
  }
}
```

If the namespace fails to resolve, prefix any item with `sekeidesign/shad-fx/`
to read the GitHub repo directly. Files land under `components/shad-fx/` and
`hooks/`, following the project's aliases. With the full library, import from
`@/components/shad-fx`; otherwise import `DitherCanvas` from
`@/components/shad-fx/dither` and each effect from
`@/components/shad-fx/effects/<name>`.

| Item | Pulls in |
| --- | --- |
| `shad-fx` | Every renderer and effect, plus an index.ts barrel |
| `shad-fx-dither` | The dither renderer, DitherCanvas. Engine, the reduced-motion hook, utils |
| `shad-fx-fire` | Engine |
| `shad-fx-rings` | Engine |
| `shad-fx-beam` | Engine |
| `shad-fx-bolt` | Engine |
| `shad-fx-fluid` | Engine |
| `shad-fx-rain` | Engine |
| `shad-fx-snow` | Engine |
| `shad-fx-engine` | Nothing — frame loop, the effect contract, seeded RNG, colour helpers |
| `use-prefers-reduced-motion` | Nothing |

## Usage

```tsx
import { DitherCanvas, fire } from "@/components/shad-fx";
import { useMemo } from "react";

export function Card() {
  const effect = useMemo(() => fire({ colors: ["#e5343a", "#f05100", "#fcbb00"] }), []);

  return (
    <div className="relative overflow-hidden rounded-lg">
      <DitherCanvas effect={effect} />
      <p className="relative">Burning</p>
    </div>
  );
}
```

Four rules, each the cause of a common bug:

1. **The parent is positioned and clips.** The canvas is `absolute inset-0`
   and fills its nearest positioned ancestor. Give the parent `relative` and
   `overflow-hidden`.
2. **Content sits above it.** Give text and controls `relative` (or a
   `z-index`), or a positioned canvas paints over them.
3. **The effect is built once.** A new `effect` reference restarts the
   simulation. Use `useMemo` with the reactive options as dependencies, or
   module scope when they are constant. Never call `fire()` inline in JSX.
4. **The component that builds the effect is a client component.** An effect
   is an object of functions and cannot cross the server/client boundary.

### DitherCanvas props

| Prop | Default | Notes |
| --- | --- | --- |
| `effect` | — | The effect to run. Keep the reference stable. |
| `active` | `true` | Eases in and out. Drive it from hover for a reveal. |
| `cell` | `2` | CSS px per dither cell. Lower is finer and costlier. |
| `seed` | `1` | Seeds the RNG, so a given seed replays identically. |
| `maxCols / maxRows` | `640 / 400` | Ceiling on the backing grid. |
| `className` | — | Merged onto the wrapper. |

Drive `active` from hover or visibility for a reveal. Do not mount and unmount
the canvas, which throws the simulation away.

## Effects

Every effect is a factory returning an `FxEffect`, and every option is
optional. The same effect runs on any renderer. `RgbInput` is a hex string or
an `[r, g, b]` tuple; CSS variables do not work, because the renderer writes
raw bytes. Where a count is given at
full intensity, it scales down as the effect eases out.

`origin` and `target` take an `Anchor`: an `[x, y]` pair in 0–1 of the box,
or a getter. `height`, `level` and `slant` take a number or a getter. A
getter is re-read every frame, so an effect can follow a pointer without being
rebuilt. Feed it a ref, not state, so pointer moves do not re-render.

### fire

Rising flame front with embers.

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `colors` | `[RgbInput, RgbInput, RgbInput]` | `["#e5343a", "#f05100", "#fcbb00"]` | Cold to hot: the tips, the body, the base. |
| `height` | `number \| (() => number)` | `0.5` | Fraction of the height the flames reach at full intensity. A getter is re-read every frame. |
| `rate` | `number` | `36` | Simulation steps per second. Lower reads chunkier. |
| `embers` | `number` | `8` | Embers aloft at once, at full intensity. |

### bolt

Lightning on a randomised interval.

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `color` | `RgbInput` | `"#fcbb00"` | Colour of the strike and its afterglow. |
| `interval` | `[number, number]` | `[0.6, 1.4]` | Seconds between strikes at full intensity, as a `[min, max]` range. |
| `target` | `Anchor` | `[0.5, 0.43]` | What the strikes aim for. They land just short of it or on it. |
| `rate` | `number` | `30` | Simulation steps per second. Lower reads chunkier. |

### rings

Sonar rings pulsing from an anchor.

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `color` | `RgbInput` | `"#ac4bff"` | Colour of the rings. |
| `origin` | `Anchor` | `[0.5, 0.42]` | The point every ring expands from. |
| `interval` | `number` | `1.15` | Seconds between rings. |
| `speed` | `number` | `0.45` | Expansion speed as a fraction of the height per second. |
| `width` | `number` | `2.6` | Ring thickness in cells. |

### fluid

A sloshing level with rising bubbles.

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `color` | `RgbInput` | `"#3080ff"` | Colour of the liquid and its bubbles. |
| `level` | `number \| (() => number)` | `0.2` | Resting depth as a fraction of the height, at full intensity. A getter is re-read every frame. |
| `slosh` | `number` | `0.09` | How far the surface tilts at either edge, as a fraction of the height. |
| `tempo` | `number` | `0.15` | Slosh cycles per second. |
| `bubbles` | `number` | `12` | Bubbles rising at once, at full intensity. |

### beam

A cone of light with drifting motes.

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `color` | `RgbInput` | `"#3080ff"` | Colour of the light and its motes. |
| `origin` | `Anchor` | `[0.5, 0.5]` | Only the `x` is used: where the light enters at the top edge. |
| `target` | `Anchor` | `undefined` | Only the `x` is used: where the axis meets the bottom edge. Unset, the beam falls straight down from `origin`. Set, it leans toward this point. |
| `spread` | `number` | `0.5` | Half-width of the cone at the bottom edge, as a fraction of the width. |
| `motes` | `number` | `16` | Dust motes drifting in the light. |

### rain

Slanted streaks, splashing on the floor.

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `color` | `RgbInput` | `"#bedbff"` | Colour of the drops and splashes. |
| `drops` | `number` | `64` | Drops in flight at once, at full intensity. |
| `speed` | `number` | `1.4` | Fall speed of the nearest drops as a fraction of the height per second. |
| `slant` | `number \| (() => number)` | `0.25` | Cells drifted sideways per cell fallen. Negative blows left. A getter is re-read every frame. |
| `length` | `number` | `6` | Streak length of the nearest drops, in cells. |

### snow

Drifting flakes that settle into a drift.

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `color` | `RgbInput` | `"#d1d5dc"` | Colour of the flakes and the drift. |
| `flakes` | `number` | `40` | Flakes aloft at once. |
| `speed` | `number` | `0.12` | Fall speed of the nearest flakes as a fraction of the height per second. |
| `sway` | `number` | `0.6` | Sideways wander as a fraction of the fall speed. |
| `settle` | `number` | `0.12` | Depth the snow settles to along the floor, as a fraction of the height. `0` for none. |

## Reduced motion

Handled. Under `prefers-reduced-motion: reduce` each effect paints one settled
frame and the frame loop parks. Do not gate the canvas on it yourself.

## Cost

The engine runs `requestAnimationFrame` only while something is changing and
parks once the effect settles, so an inactive effect costs nothing. Each frame
is one `putImageData` over a grid capped at 640×400 cells. Use `cell={1}` on
small elements and `3` or `4` on a full-bleed hero.

## Agent skills

```bash
npx skills add sekeidesign/shad-fx
```

Install them when the project will keep working with shad-fx.

- `use-shad-fx`: Put an effect on a card, hero, button or section, or fix one that restarts, flickers, hides the content or costs too much. Covers placement, keeping the `effect` reference stable, anchors, colours and cost.
- `create-shad-fx`: Write an effect the shipped set doesn't cover, like smoke, sparks or static, or change how one simulates. Covers the `FxEffect` contract, the `Surface` it paints into, reduced motion and parking.
