KQ-1354 · GRAIN/GATHER

Grain Gather

Particle text poured from sand: the words are laid out for real in the DOM, drawn from their own glyphs into an offscreen mask and sampled into a few thousand grains on a canvas, which start scattered in a slow drift and gather into the letters in a left-to-right sweep on per-grain springs from the springs.drift calibration, a touch under-damped so the words sharpen out of a haze. A pointer moving through the words parts the grains like a hand through sand and they flow back behind it; a press throws them outward and they regather, and Enter or Space on the focused text does the same from its centre.

KQ-1354 · GRAIN/GATHER

Fernworks · studio pottery

Thrown from river clay

Move across the words to part the grains; press, or press Enter, to scatter them.

gathering · the grains are settling

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/grain-gather

Props

text
string
default —
The words the grains gather into.
as
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "div"
default "h2"
The text element rendered.
density
number
default 0.6
How many grains, 0 to 1: about 1,200 to 4,800, spread over the letters' ink.
radius
number
default 56
How far the hand reaches, px.
strength
number
default 0.6
How hard the hand pushes the grains aside, 0 to 1.
scatter
number
default 0.6
How hard a press throws the grains, 0 to 1.
drift
number
default 0.5
How much the scattered grains wander before they gather, 0 to 1.
color
string
default "currentColor"
The grains' colour; by default the text colour you set.
grain
number
default 1.3
A grain's size, px; large type scales it up so the sand stays dense.
dpr
number
default 2
The highest device pixel ratio the canvas draws at (never above 2).
interactive
boolean
default true
A pointer parts the grains, a press scatters them, and Enter or Space scatters them from the keyboard.
trigger
"mount" | "inView" | "hover" | "manual"
default "inView"
When they gather: on mount, when scrolled into view, on hover or keyboard focus, or when active turns true.
once
boolean
default true
Gather only the first time; false lets them drift apart when the text leaves view and gather again.
delay
number
default 100
Wait before the grains start to gather, ms.
active
boolean
default —
For trigger="manual": true gathers the grains, false lets them drift apart.
onComplete
() => void
default —
Fires the first time the grains settle into the words.
className
string
default —
Classes for the wrapper; set the type size, weight and colour here.