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