breathe

The paragraph
breathes.

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

Each line of a paragraph oscillates its letter-spacing at a fixed phase offset from its neighbours. The result is a slow ripple — a wave of expansion and contraction moving through the text. At low amplitudes it reads as living rather than animated.

Live demo — watch the paragraph

Amplitude0.012
Period (s)3.5
Phase offset0.79
WaveAxis

Hold still and watch the paragraph. Each line is breathing at its own pace — expanding and contracting its letter-spacing in a slow oscillation, offset from its neighbours by a fixed phase angle. The top lines and the bottom lines never breathe together. A wave moves through the paragraph rather than a pulse. At the default amplitude the movement is almost subliminal: you notice something alive before you notice what it is. Increase the amplitude to see the mechanics. The wave shape changes the character of the motion — sine is smooth, triangle is more mechanical. The period controls how fast each line completes its cycle.

Each line oscillates at ±0.012 em, period 3.5s, phase offset 0.79 rad per line.

How it works

Phase offset per line

The algorithm detects visual lines and assigns each a phase position: line 0 is at phase 0, line 1 at phaseOffset radians, line 2 at 2× phaseOffset, and so on. Each frame, the wave function is evaluated at each line's current phase.

Letter-spacing or wdth axis

The oscillation can drive either CSS letter-spacing (for any font) or the variable font wdth axis (for variable fonts that support it). The wdth axis gives a more physical quality — the letterforms themselves change shape rather than just spacing.

Usage

Drop-in component

import { BreatheText } from '@liiift-studio/breathe'

<BreatheText amplitude={0.012} period={3.5} phaseOffset={0.785}>
  Your paragraph text here...
</BreatheText>

Options

OptionDefaultDescription
amplitude0.012Peak letter-spacing change in em (or wdth units).
period3.5Seconds per full oscillation cycle.
phaseOffsetπ/4Phase shift between adjacent lines in radians.
waveShape'sine''sine' | 'triangle'
axis'letter-spacing''letter-spacing' | 'wdth' (variable font axis)