floodtext

Character
by character.

npm ↗
GitHub
TypeScript·Zero dependencies·React + Vanilla JS

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

Amplitude (wght units)200
Period (s)4
Density2
EffectWaveDirection

A wave washes through the paragraph — not line by line, not word by word, but character by character. Every letterform sits at its own position in the curve: weight surges as the wave crests and falls as it troughs, oblique angles tilt and recover, opacity breathes through each glyph in sequence. The text is the same, but it is no longer still.

CSS applies properties to elements, not to individual characters. Font variation settings, opacity, transforms — all or nothing, the entire block at once. Flood Text works around this by wrapping each visible character in its own span, evaluating the wave function at that character's normalised position, and writing the result as an inline style. Whitespace is left as bare text nodes and never touched — no layout impact, no reflow.

At low amplitude the effect is texture: a subtle restlessness the reader feels before they name it, like the slight variation in hand-set type. At high amplitude it becomes transformation — weight swinging from hairline to black, letters tilting into italics and back, the whole paragraph in motion. Density controls how many wave cycles are visible at once; period controls the tempo. Layer wght with oblique, or opacity with wdth, and the motion compounds into something no single CSS property could produce.

A sine wave traveling top-left to bottom-right through 1079 characters — ±200 wght units on wght, density 2, period 4s.

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

OptionDefaultDescription
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.
amplitudeautoPeak 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.
amplitudesPer-effect amplitude overrides when layering multiple effects. E.g. { wght: 300, blur: 3 }.
propertiesAnimate 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 }].
period4Seconds per full wave cycle.
density2Wave 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'