Install
Take the whole library, or a renderer and the effects you want:
npx shadcn@latest add @sekei/shad-fx
npx shadcn@latest add @sekei/shad-fx-dither @sekei/shad-fx-fireAn effect on its own has nothing to draw on, so always take a renderer with it.
Nothing to configure. @sekei is in the shadcn registry directory, so the CLI resolves it and writes the registries entry into your components.json itself. To pin it yourself, by hand or in package.json — the CLI reads both:
{
"registries": {
"@sekei": "https://www.sekei.xyz/registry/{name}.json"
}
}Files land under components/shad-fx/ and hooks/, following your components.json aliases: renderers in dither/, effects in effects/. With the full library, import everything from @/components/shad-fx. With a renderer and a few effects, import from @/components/shad-fx/dither and @/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 |
If the namespace ever fails to resolve, the CLI can read the repo directly: prefix any item with sekeidesign/shad-fx/.
Usage
To have an agent set effects up for you, install the agent skills. One covers placement, the stable-reference rule, anchors and cost; the other writes new effects.
npx skills add sekeidesign/shad-fxThe canvas fills its nearest positioned ancestor, so give the parent relative.
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>
);
}Build the effect once. A new effect reference restarts the simulation — the canvas and its observer survive, but particles and heat fields reset. Use useMemo with the options in the dependency array, or module scope when the options are constant.
| 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. |
The element is aria-hidden and pointer-events-none: it is decoration, and never the only carrier of meaning.
Effects
To write an effect this set doesn't cover, have an agent do it with the agent skills. They cover the FxEffect contract, the Surface an effect paints into, reduced motion and parking.
npx skills add sekeidesign/shad-fxEvery 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. Where a count is given at full intensity, it scales down as the effect eases out.
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
origin and target take an Anchor: an [x, y] pair in 0–1 of the box, or a getter, which is re-read every frame — so an effect can follow something that moves without being rebuilt and losing what it has already simulated. Drag the dot in the playground above to see it.
Agent skills
Two skills, installed together, for an agent working in a project that uses shad-fx.
npx skills add sekeidesign/shad-fx| Skill | Use it to | Covers |
|---|---|---|
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. | 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. | The FxEffect contract, the Surface it paints into, reduced motion and parking. |
Reduced motion
The canvas reads prefers-reduced-motion through useSyncExternalStore, so it is correct on the server and updates when the setting changes. Under reduce, each effect paints one settled frame and stops: fire is pre-warmed and still, rings sit at three fixed radii, rain and snow hang mid-fall, particles are dropped. Nothing animates and the frame loop parks.
Cost
The engine runs requestAnimationFrame only while something is changing, and stops once the eased intensity has settled and the effect reports idle(). An inactive effect costs nothing. Each frame is one putImageData over a grid capped at 640×400 cells, not a fillRect per cell.
Credit
The ordered-dither rendering here, meaning the low-resolution backing canvas scaled up pixelated, the Bayer threshold matrix, and filling every cell at one of two alpha tiers rather than leaving holes, derives from dither-kit under the MIT licence. The effects, the painter, the frame loop and the reduced-motion handling are not.