floodtext
Character
by character.
A wave washes through the body copy character by character — modulating weight, width, oblique angle, or opacity as it passes. Not line by line, not word by word: every letterform sits at its own moment in the curve. At low amplitude it reads as texture; at high amplitude, as transformation.
Live demo — watch the paragraph
How it works
Per-character phase
Every visible character is wrapped in an inline span. Each frame, the wave function is evaluated at that character's position in the text — normalised across the whole paragraph. The density option controls how many wave cycles are visible at once.
Traveling wave
The wave advances through the characters over time using a requestAnimationFrame loop. Speed is consistent regardless of display refresh rate. The loop cleans up on unmount. Whitespace is left as bare text nodes — no layout impact, no reflow.
Usage
Drop-in component
import { FloodText } from '@liiift-studio/floodtext'
<FloodText effect="wght" amplitude={200} period={4} density={2} direction="diagonal-down">
Your paragraph text here...
</FloodText>Hook
import { useFloodText } from '@liiift-studio/floodtext'
const ref = useFloodText({ effect: 'wght', amplitude: 200, period: 4, density: 2, direction: 'diagonal-down' })
<p ref={ref}>{children}</p>Vanilla JS
import { applyFloodText, startFloodText, removeFloodText, getCleanHTML } from '@liiift-studio/floodtext'
const el = document.querySelector('p')
const original = getCleanHTML(el)
const chars = applyFloodText(el, original, { effect: 'wght', amplitude: 200, period: 4, density: 2, direction: 'diagonal-down' })
const stop = startFloodText(chars, { effect: 'wght', amplitude: 200, period: 4, density: 2, direction: 'diagonal-down' })
// Later — stop animation and restore:
stop()
removeFloodText(el, original)Options
| Option | Default | Description |
|---|---|---|
| effect | 'wght' | 'wght' | 'wdth' | 'oblique' | 'opacity' | 'rotation' | 'blur' | 'size'. Pass an array to layer multiple effects simultaneously. Note: oblique requires Chrome 87+, Firefox 88+, Safari 14.1+. size causes layout recalculation per frame — use low amplitude. |
| amplitude | auto | Peak deviation from neutral. Used for single-effect mode. Defaults: wght 200, wdth 20, oblique 15deg, opacity 0.3, rotation 15deg, blur 2px, size 0.15em. |
| amplitudes | — | Per-effect amplitude overrides when layering multiple effects. E.g. { wght: 300, blur: 3 }. |
| properties | — | Animate any CSS property or CSS custom property on each character, driven by the same wave. Each entry: { property, base, amplitude, unit?, clamp? }. Works with CSS variables. E.g. [{ property: 'letter-spacing', base: 0, amplitude: 0.05, unit: 'em' }] or [{ property: '--my-axis', base: 100, amplitude: 20 }]. |
| period | 4 | Seconds per full wave cycle. |
| density | 2 | Wave cycles visible across the paragraph at once. Higher = more bands. |
| direction | 'diagonal-down' | 'diagonal-down' ↘ | 'diagonal-up' ↗ | 'right' → | 'left' ←. Diagonal directions use 2D character positions. |
| waveShape | 'sine' | 'sine' | 'sawtooth' | 'triangle' |