Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Particle Field

A deterministic, very-low-alpha canvas field of drifting phosphor dots — hero atmosphere only.

Status
stable
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-field

The 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

PropTypeDefaultDescription
countnumber48Particle count. Capped at PARTICLE_FIELD_MAX_COUNT regardless of the value passed — stays subtle and performant by construction.
seednumber1Deterministic 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

AttributeValues
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: reduce by rendering one static frame — which still follows a theme change, so a reduced-motion user never sees the wrong-theme ink.
ContractStates tested
Behaviourdefault, empty, loading
Accessibilitynative-or-base-ui-semantics, browser-accessibility-test
Visualdefault, loading, empty

Do / Don't

Do
Use it as subtle hero-background atmosphere over a dark marketing ground — keep the particle count and alpha low.
Don't
Use it as a focal element, or stack more than one ParticleField in the same viewport.

On this page