Usage
An open-code shadcn primitive. The CLI writes the component and its canvas runtime into your project; there is no package to depend on.
Install
Needs React 18 or 19, a shadcn components.json, and a logo as SVG, PNG, JPG or WebP.
@benday is a namespace in shadcn’s registry directory, so nothing goes in your components.json. The code lands at components/ui/benday.tsx and components/ui/benday/*, and is yours to edit.
Manual install
No CLI? Copy the seven files out of the registry payload by hand. Nothing generates them, and the imports between them are relative, so the tree is all that matters.
components/ui/├── benday.tsx the component and the public exports└── benday/ ├── bake.ts image → dot map ├── dom.ts colour, theme, visibility, DPR ├── presets.ts the twenty-one animations ├── renderer.ts the canvas painter ├── types.ts every exported type └── use-dot-map.ts the React bindingEvery file’s source is the content field of /r/benday.json. Only React is required — there is nothing else to install.
Quick start
Point it at an image. It bakes the logo into a dot map on mount, then animates it.
import { Benday } from "@/components/ui/benday";
export function Example() { return <Benday src="/logo.svg" size={64} />;}currentColor is the default and re-resolves when the theme flips, so the mark tracks surrounding text on its own.
Tune the bake
The bake runs once per URL and options, then caches. Transparency is used when present; opaque images fall back to a luminance mask.
<Benday src="/logo.svg" bake={{ grid: 24, dilate: 1 }} />The playground is the fast way to find settings that hold up at your smallest size.
Presets
Twenty-one motions across signature, sweep, orbit, field and transform families. Every one preserves your logo.
| contour | SignatureA wave follows the mark’s thickness from outline to core. |
|---|---|
| shimmer | SignatureA lit band sweeps across the mark on the diagonal. |
| ripple | SignatureConcentric rings pulse outward from the center. |
| breathe | SignatureThe whole mark swells and settles with a soft edge delay. |
| scan | SweepA narrow vertical beam traverses the silhouette. |
| cascade | SweepOne signal follows the lattice in serpentine order. |
| weave | SweepCounter-moving diagonal bands cross through the mark. |
| rain | SweepOffset droplets descend each column with soft tails. |
| swirl | OrbitThe mark twists around its center, with outer dots lagging. |
| orbit | OrbitA soft energy point circles the center of the mark. |
| comet | OrbitA bright head and tapered tail chase around the mark. |
| radar | OrbitA rotating search beam crosses the logo with a soft wake. |
| pinwheel | OrbitThree curved blades rotate around a steady core. |
| beacon | OrbitEmphasis hands off between the four cardinal directions. |
| flicker | FieldA stable random subset of dots blinks at any moment. |
| wave | FieldA soft traveling current bends the dot lattice. |
| equalizer | FieldIndependent column levels rise and fall like a spectrum. |
| resolve | FieldDots assemble in stable random order, then dissolve. |
| scatter | TransformDots drift off the lattice, then reconverge into the mark. |
| magnetic | TransformThe field pulls toward its center and releases. |
| glitch | TransformBrief horizontal faults disturb a few rows, then clear. |
A preset is a function of one dot and the clock, so you can pass your own instead of a name.
States
thinking runs the preset; idle and done show the crisp mark. Switching springs the dots back into the logo.
const state = isStreaming ? "thinking" : "done";
<Benday src="/logo.svg" state={state} />;Bake ahead of time
The bake is the only expensive step, and it does not need to happen in the browser. Run it once at build time, commit the dot map, and the client just paints.
// scripts/bake-logo.ts — run once, commit the JSON.import { writeFile } from "node:fs/promises";import { bake } from "@/components/ui/benday";
const dotMap = await bake("./public/logo.svg", { grid: 24 });await writeFile("./src/logo-dots.json", JSON.stringify(dotMap));import dotMap from "@/logo-dots.json";
<Benday dotMap={dotMap} size={64} />;dotMap wins over src, so nothing rasterizes on mount and the mark is there on the first frame. A 24-dot map is a few KB of JSON. bake draws through a canvas, so the script needs a DOM — run it under Playwright, jsdom with node-canvas, or any browser context. Settle the bake options in the playground first, then pass the same ones here.
Use in real UI
Put the indicator next to what is pending, at the size that thing occupies.
import { Benday } from "@/components/ui/benday";
export function SaveButton({ isSaving }: { isSaving: boolean }) { return ( <button className="inline-flex items-center gap-2 rounded-md border px-3 py-2" disabled={isSaving} type="button" > {isSaving ? ( <Benday src="/logo.svg" size={18} bake={{ grid: 10 }} /> ) : null} <span>{isSaving ? "Saving…" : "Save changes"}</span> </button> );}Note the grid override: at 18px a 24-dot grid gives sub-pixel dots that read as grey fuzz.
Props
| Prop | Type | Default |
|---|---|---|
| srcURL, data URI, File/Blob, or a loaded HTMLImageElement | BakeSource | None |
| dotMapA pre-baked map. Takes precedence over src | DotMap | None |
| bakegrid, threshold, gamma, dilate, maskMode, invert, trim | BakeOptions | None |
| sizeCSS pixels | number | 64 |
| fit'natural' sizes to the mark, which is what wordmarks need | 'square' | 'natural' | 'square' |
| statethinking runs the preset; the others settle to the crisp mark | BendayState | 'thinking' |
| presetA preset name or your own per-dot function | PresetName | Preset | 'contour' |
| speedMultiplier | number | 1 |
| colorResolved off the canvas, re-resolved on theme change | string | 'currentColor' |
| dotScaleDot diameter as a fraction of the grid cell | number | 0.62 |
| shapecircle, square or diamond | DotShape | 'circle' |
| glowHalo radius as a fraction of the dot | number | 0 |
| paddingInset as a fraction of the box | number | 0.06 |
| weightHow strongly ink coverage drives dot size | number | 0.5 |
| pausedFreeze on the current frame | boolean | false |
| reducedMotion'auto' follows prefers-reduced-motion | boolean | 'auto' | 'auto' |
Guidelines
- Coarser grid for smaller sizes. Below roughly 1.6px per dot the mark stops reading.
- Thin strokes vanish. A dilate of 1 to 3px rescues a hairline logo; drop threshold alongside it.
- Wide wordmarks want fit="natural", or a square letterboxes away half their size.
- One indicator per pending region. Several at once compete.
- Motion is decorative: role="img" with a per-state label, and one static frame under prefers-reduced-motion.
Next step
Throw your ugliest logo at it and see what survives.