Particle Field Hero
A hero over an interactive particle field: particles drift and link up when close, the pointer pushes them away, and the headline stays legible on top.
A full-bleed canvas particle field fills the background. Each particle gets its starting position and direction from a seed, drifts 0.15–0.4px per frame and bounces off the edges; any two particles closer than linkDistance are joined by an accent-colored line that fades with distance. When the pointer comes within 120px it pushes particles away, and they slip back into their drift once it leaves. A radial mask fading from the background color to transparent sits under the headline, so the text keeps its contrast however dense the field gets. Optionally, a glass-framed photo card sits on the right.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add particle-field-hero.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ParticleFieldHero/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/particle-field-hero.en.jsonOr, once registries are configured in components.json, install it as @webberui/particle-field-hero.
Usage
import { ParticleFieldHero } from "@/components/ui/particle-field-hero";
<ParticleFieldHero
eyebrow="潮汐感測平台 · 公開測試中"
title="把每一個感測點連成一張網"
subtitle="千台裝置的即時讀數在同一個畫面流動。"
primaryAction={{ label: "申請試用", href: "/signup" }}
secondaryAction={{ label: "看架構說明", href: "/docs" }}
count={100}
linkDistance={120}
showMedia
images={[{ src: "/hero/sensor.jpg", alt: "海岸邊的感測站" }]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | Headline (required) |
eyebrow | ReactNode | — | Small label above the headline, shown in the accent color |
subtitle | ReactNode | — | Supporting text below the headline |
primaryAction | ParticleFieldHeroAction | — | Primary CTA with an accent background and an arrow icon |
secondaryAction | ParticleFieldHeroAction | — | Secondary CTA, outlined |
as | "h1" | "h2" | "h1" | Heading level; use "h2" when the page already has an h1 |
minHeight | number | 560 | Minimum section height (px) |
align | "center" | "left" | "center" | Text alignment; always left when showMedia is on |
count | number | 80 | Maximum particle count; the actual count is capped at one per 12,000px² of area |
linkDistance | number | 110 | Particles closer than this (px) get a link whose opacity fades with distance; 0 disables links |
repel | boolean | true | Push particles away from the pointer within 120px |
speed | number | — | Particle speed multiplier, written to --wb-particle-field-hero-speed; falls back to the CSS variable (default 1) |
duration | number | — | Text entrance duration (ms), written to --wb-particle-field-hero-duration; falls back to the CSS variable (default 600) |
seed | number | 1 | Random seed for initial positions and directions; the same seed gives the same layout |
showMedia | boolean | false | Show a glass-framed photo card on the right |
images | ParticleFieldHeroImage[] | — | Photo card image (only the first is used); without src a gradient + SVG geometric placeholder is drawn |
className | string | — | Merged into the root section |
ParticleFieldHeroAction
| Field | Type | Default | Description |
|---|---|---|---|
label | string | — | Button text |
href | string | — | Renders an <a> when provided, otherwise a <button type="button"> |
onClick | (event: React.MouseEvent<HTMLElement>) => void | — | Click handler |
ParticleFieldHeroImage
| Field | Type | Default | Description |
|---|---|---|---|
src | string | — | Your own image URL; a placeholder is shown when omitted |
alt | string | — | Alt text (required) |
How it works
- Particles come from a mulberry32 PRNG seeded with
seed, so positions and directions are identical every time; changingspeeddoes not reseed, so nothing jumps. - The repel is only a draw-time offset that is never written back to the particle, so once the pointer leaves, particles carry on along their original path. Pointer position and repel strength ease at 0.12 per frame for smooth entry and exit. Only mouse and pen respond; touch drags don't trigger it.
- Colors are read with
getComputedStyleinside the effect: particles use the canvascurrentColor(neutral-400 light, neutral-500 dark) and links use the accent at 25% opacity; both are re-read when the theme changes. - Canvas resolution is capped at DPR 2 and follows its size through a ResizeObserver; rAF pauses when the field scrolls out of view (IntersectionObserver) or the tab is hidden (visibilitychange), and everything is cleaned up on unmount.
- Overridable CSS variables:
--wb-particle-field-hero-accent(default#0d9488,#2dd4bfin dark mode),--wb-particle-field-hero-speed,--wb-particle-field-hero-duration. - Entrance: the canvas fades in over 800ms; eyebrow, headline, subtitle and CTAs rise 16px and fade in, 80ms apart, easing
cubic-bezier(0.22,1,0.36,1).
Accessibility
- The root is a
<section aria-labelledby>pointing at the headline'suseId. - The canvas and radial mask are
aria-hiddenand purely decorative; the placeholder photo card usesrole="img"with itsalttext. - CTAs are
<a>elements ortype="button"buttons with a focus-visible ring. - With "reduce motion" enabled, a single static frame of particles and links is drawn: no rAF loop and no repel. The text entrance becomes a 150ms fade with no stagger.
Blur-In Headline Hero
A minimal centred React hero where the headline rises character by character out of a blur and the single call-to-action breathes with a slow halo while everything else stays still; Chinese splits per character, English keeps words intact, and every timing is a CSS variable.
Browser Frame Hero
A hero with one large browser-style device frame filling the right side, which straightens and scales up from a slight tilt as the page scrolls, holding any product screen you put inside.