Particle Field
A deterministic, very-low-alpha canvas field of drifting phosphor dots — hero atmosphere only.
- Status
- Since
0.1.0- Accessibility pattern
- decorative canvas (aria-hidden)
Last updated
Hero atmosphere
A deterministic, very-low-alpha phosphor field — decorative only, lazy-mounted.
Install
Add Particle Field from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/particle-fieldThe same command installs the registry items it composes: @vegastack/use-media-query.
Usage
import { ParticleField } from "@/components/ui/particle-field";
<div className="relative">
<ParticleField seed={7} count={48} />
<h1 className="relative">Ship agentic UI, fast.</h1>
</div>;ParticleField is absolutely positioned to fill its parent (inset-0) — wrap it in a relative
container and stack your real content above it (relative + normal document order, or z-*).
Examples
Hero atmosphere
A deterministic, very-low-alpha phosphor field — decorative only, lazy-mounted.
Lazy by construction
The draw/animate effect only starts once the element is intersecting the viewport and the
browser reports idle (requestIdleCallback, falling back to setTimeout(0) where unsupported) —
a hero hidden below the fold never spends a frame budget before it's visible. For true
code-splitting (not loading the component's JS at all until needed), wrap the import at the
call site in next/dynamic(() => import(...), { ssr: false }) — see the docs-home hero for the
reference usage.
Determinism
The layout is deterministic per seed, so the same seed always paints the same field and a
screenshot never drifts.
seed 3 · 24
seed 11 · 96
Reduced motion & theme
Renders exactly one static frame (no requestAnimationFrame loop at all) under
prefers-reduced-motion: reduce, repainted when the theme changes so this branch follows the
theme too. The particle ink is the canvas's own resolved color (it carries text-brand), read
per frame rather than captured once at mount — so toggling light/dark recolours the field in
place, with no remount. Particle positions come from a seeded PRNG (mulberry32) — never
Math.random() — so the same seed always produces the same layout. count is capped
(PARTICLE_FIELD_MAX_COUNT) regardless of the value passed, so it stays subtle and performant by
construction. The root element carries data-drawn once the first frame has actually painted —
wait on that attribute instead of guessing a delay (a VRT harness should).
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | 48 | Particle count. Capped at PARTICLE_FIELD_MAX_COUNT regardless of the value passed — stays subtle and performant by construction. |
seed | number | 1 | Deterministic PRNG seed. The same seed always produces the same particle layout — change it to get a different (but still stable) field. |
Data attributes and CSS variables on ParticleField
| Attribute | Values |
|---|---|
data-drawn | "" |
data-slot | "particle-field" | "particle-field-canvas" |
Accessibility
- Purely decorative — rendered
aria-hidden="true"in full; it never carries content a screen reader needs. - Respects
prefers-reduced-motion: reduceby rendering one static frame — which still follows a theme change, so a reduced-motion user never sees the wrong-theme ink.
| Contract | States tested |
|---|---|
| Behaviour | default, empty, loading |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test |
| Visual | default, loading, empty |