FlowField

A React animated background component: a canvas needle field that bends away from the pointer. For hero sections.

FlowField playground

Sweep the pointer across itThe needles breathe on a noise loop until the pointer arrives, then swing away with per-cell lag. The grip takes hold in 83ms and lets go over 400ms, so the field never snaps back.

Install FlowField

1. New project? One command installs the package and wires everything - see Installation:

bash
pnpm dlx zyncat-ui init

2. Import the component - it loads its own stylesheet:

tsx
import { FlowField } from '@zyncat/ui/flow-field';

Usage

tsx
import { FlowField } from '@zyncat/ui/flow-field';
 
<FlowField spacing={26} radius={210}>
<Hero />
</FlowField>

Compose FlowField in your application. No Tailwind or external styling library is required; every value resolves from Zyncat UI's token vocabulary.

FlowField props

FlowField

PropTypeDefaultDescription
...htmlAttributesHTMLAttributes—Standard <div> attributes (aria-*, data-*, title, ...) forwarded to the root.
childrenReactNode—Content layered over the field. It stays in the accessibility tree; only the canvas is aria-hidden.
speednumber1Multiplies the simulation rate; sampled live on every frame.
spacingnumber26Distance in pixels between neighbouring needles, clamped to 12-72. Widened automatically on large surfaces so the field never exceeds 1600 needles.
radiusnumber210Radius in pixels of the pointer's steering influence, clamped to 40-640.
classNamestring—Extra class(es) merged onto the root.
styleFlowFieldStyle—Inline styles merged onto the root.

FlowFieldStyle

PropTypeDefaultDescription
--flow-field-inkstring | number—`--flow-field-ink` - The particle colour at rest. Default: `var(--text-subtle)`.
--flow-field-accentstring | number—`--flow-field-accent` - The particle colour at full speed. Default: `var(--accent)`.
--flow-field-min-heightstring | number—`--flow-field-min-height` - The field's minimum height before content sizes it. Default: `var(--space-10)`.
--flow-field-ramp-0string | number—`--flow-field-ramp-0` - The twelve stops from ink to accent, set together for a ramp of your own. Default: `var(--flow-field-ink)`.
--flow-field-ramp-1string | number—`--flow-field-ramp-1` - The twelve stops from ink to accent, set together for a ramp of your own. Default: `color-mix(in oklab, var(--flow-field-accent) 9%, var(--flow-field-ink))`.
--flow-field-ramp-2string | number—`--flow-field-ramp-2` - The twelve stops from ink to accent, set together for a ramp of your own. Default: `color-mix(in oklab, var(--flow-field-accent) 18%, var(--flow-field-ink))`.
--flow-field-ramp-3string | number—`--flow-field-ramp-3` - The twelve stops from ink to accent, set together for a ramp of your own. Default: `color-mix(in oklab, var(--flow-field-accent) 27%, var(--flow-field-ink))`.
--flow-field-ramp-4string | number—`--flow-field-ramp-4` - The twelve stops from ink to accent, set together for a ramp of your own. Default: `color-mix(in oklab, var(--flow-field-accent) 36%, var(--flow-field-ink))`.
--flow-field-ramp-5string | number—`--flow-field-ramp-5` - The twelve stops from ink to accent, set together for a ramp of your own. Default: `color-mix(in oklab, var(--flow-field-accent) 45%, var(--flow-field-ink))`.
--flow-field-ramp-6string | number—`--flow-field-ramp-6` - The twelve stops from ink to accent, set together for a ramp of your own. Default: `color-mix(in oklab, var(--flow-field-accent) 55%, var(--flow-field-ink))`.
--flow-field-ramp-7string | number—`--flow-field-ramp-7` - The twelve stops from ink to accent, set together for a ramp of your own. Default: `color-mix(in oklab, var(--flow-field-accent) 64%, var(--flow-field-ink))`.
--flow-field-ramp-8string | number—`--flow-field-ramp-8` - The twelve stops from ink to accent, set together for a ramp of your own. Default: `color-mix(in oklab, var(--flow-field-accent) 73%, var(--flow-field-ink))`.
--flow-field-ramp-9string | number—`--flow-field-ramp-9` - The twelve stops from ink to accent, set together for a ramp of your own. Default: `color-mix(in oklab, var(--flow-field-accent) 82%, var(--flow-field-ink))`.
--flow-field-ramp-10string | number—`--flow-field-ramp-10` - The twelve stops from ink to accent, set together for a ramp of your own. Default: `color-mix(in oklab, var(--flow-field-accent) 91%, var(--flow-field-ink))`.
--flow-field-ramp-11string | number—`--flow-field-ramp-11` - The twelve stops from ink to accent, set together for a ramp of your own. Default: `var(--flow-field-accent)`.

Frequently asked questions

Import FlowField from '@zyncat/ui/flow-field' and wrap whatever should sit on top: <FlowField spacing={26} radius={210}><Hero /></FlowField>. The canvas fills the root and your children render above it, so a hero, a card or a section heading keeps its own layout and semantics. The file already carries 'use client', so it drops into the Next.js App Router with no extra directive.

A flow field gives every point on the surface a direction, and whatever is drawn there lines up with it. This background lays needles on a grid and turns each one toward its local angle, so the whole field drifts like grass in wind. Generative-art flow fields usually sample Perlin or simplex noise; this one sums a sine over x and a cosine over y with time in the phase, which loops smoothly and costs one trig pair per needle.

It is a plain 2D canvas - getContext('2d'), no WebGL context, no three.js and no shader, so nothing is compiled at runtime and there is no extra bundle to pull in. That is the difference from Vanta.js, where most effects need three.js on the page as well. It is not a CSS background either: the needles are stroked per frame rather than tweened by keyframes, and it is not a GIF or a background video.

Pointer steering is on by default and there is no prop to enable it. Every needle inside radius - 40 to 640 px, 210 by default, sampled live on each frame - turns to point away from the pointer, stretches and brightens through a twelve-stop colour ramp, and the pull falls off toward the edge of that circle. The grip is released more slowly than it is taken, so the field eases back rather than snapping when the pointer leaves.

Not quite. In a particle background such as particles.js or tsparticles the dots travel across the screen and link up; here the needles are pinned to a grid and only rotate, stretch and brighten, so the motion reads as a field rather than as drifting dust. It is also one component with zero runtime dependencies and no Tailwind, not a library plus a config object.

This one is capped rather than open-ended: at most 1600 needles, with spacing widening on its own on large surfaces, device pixel ratio capped at 2, and the loop stopping entirely when the element scrolls out of view or the tab is hidden. The canvas is aria-hidden and adds no tab stop, your children keep their own semantics, and under prefers-reduced-motion it paints one still frame of the settled field and never starts the loop.

Set in Geist & Newsreader — animated by the house engineZyncat UI · Rev 0.11 · MIT · Built by Tabsir Ahammed · Source on GitHub