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.
Accessibility & compatibility
On e-ink and slow-refresh displays (update: slow media feature — Kindle, Remarkable, and similar panels), the wave animation produces no visible effect. FloodText detects this automatically: the element is restored to its original HTML and all animation work is skipped. The prefers-reduced-motion: reduce preference is also honoured — when set, the animation is skipped entirely and the element is left in its original state. The demo above respects this preference too: if reduced motion is set in your OS, the text will remain static.
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 opts = { effect: 'wght', amplitude: 200, period: 4, density: 2, direction: 'diagonal-down' }
const chars = applyFloodText(el, original, opts)
const stop = startFloodText(chars, opts)
// 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. |
| source | 'fixed' | 'fixed' — all characters share the same amplitude. 'sentiment' — per-word AFINN emotional valence scores scale the amplitude; requires npm install sentiment, falls back to 'fixed' if not installed. |
| 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' |
| pauseOffscreen | true | Pause the animation when the element scrolls out of view; resume when visible. Uses IntersectionObserver internally. |
| as | 'p' | HTML element to render, e.g. 'h1', 'span'. Accepts any valid React element type. (FloodText component only) |
no-code
Use it in Webflow, Framer & Figma
The same effect, no build step — drop it straight into your design tool.
Webflow
One script tag, then mark any element with data-floodtext. Configure it with data-* attributes.
<!-- Site Settings → Custom Code → Footer, or an Embed element -->
<script src="https://cdn.jsdelivr.net/npm/@liiift-studio/floodtext/dist/floodtext.webflow.min.js"></script>
<!-- Then add data-floodtext to any text element -->
<h1 data-floodtext>Your headline</h1>Framer
Insert → Code → New Component, then paste FloodText.tsx ↗. It imports the core from esm.sh and exposes every option in the property panel — no build step.
import { /* core */ } from "https://esm.sh/@liiift-studio/floodtext"Figma · beta
Part of the Type Tools Figma plugin ↗ — Plugins → Development → Import plugin from manifest, run Type Tools, and pick this tool. Here it bakes a single static frame of the animated effect (no live animation in Figma).