Typsettle

letter-spacing settling


npm ↗
GitHub ↗
TypeScriptZero dependenciesReact + Vanilla JS

Paragraph text enters from randomised letter-spacing and transitions to optical equilibrium. A page-load animation that feels typographic rather than decorative — lines staggered, motion purposeful. Nothing animates under prefers-reduced-motion, on slow-refresh screens, or with active set to false.

Live demo

Spread0.04
Duration (ms)800
Stagger (ms)80
EasingDirection

Every line finds its own tracking, one after another, and settles into place.

The first thing a reader notices about a text is not the words but the colour — the even grey field of the paragraph taken as a whole. Before meaning, before syntax, there is that impression: light, dark, dense, airy. The typographer works to make it even, to give the reader a surface to move across without resistance. A page-load animation that begins in chaos and resolves into order says something about the text it introduces: that it knows where it is going.

Tracking — the spacing between letters across a whole word or line — is the most delicate of the compositor's instruments. Too tight and letters close against each other; too loose and words fragment. The right amount is invisible.

Text enters from randomised tracking and settles to equilibrium. Each line is staggered by 80ms.

How it works

The entrance

Each line of text starts from its own random letter-spacing offset, up to the spread you set: looser than its settled spacing with the default 'expand', tighter with 'compress'. The offset is different on every line, so the paragraph starts slightly uneven and reads as typographic texture rather than noise.

The resolution

A CSS transition carries each line to its natural letter-spacing baseline — the element's existing CSS tracking, if any, or zero if none is set. The stagger control spaces these transitions apart so lines settle in sequence rather than all at once. Lines are locked as the browser set them, so nothing re-wraps and nothing below the text moves while it settles.

Usage

Drop-in component

import { SettleText } from '@overpunch/typsettle'

<SettleText spread={0.04} duration={800} stagger={80}>
  Your paragraph text here...
</SettleText>

Hook

import { useSettle } from '@overpunch/typsettle'

const { ref, replay } = useSettle({ spread: 0.04, duration: 800, stagger: 80 })
return <p ref={ref}>{children}</p>

Vanilla JS

import { applySettle, removeSettle, replaySettle, getCleanHTML } from '@overpunch/typsettle'

const el = document.querySelector('p')
const original = getCleanHTML(el)
applySettle(el, original, { spread: 0.04, duration: 800, stagger: 80 })

// Replay the animation on a settled element (e.g. on a button click):
// replaySettle(el)

// Restore original markup:
// removeSettle(el, original)

Options

SettleText / useSettle / applySettle options reference
OptionDefaultDescription
spread0.04Max initial letter-spacing offset in em.
duration800Transition duration in milliseconds.
easing'cubic-bezier(0.25, 0.1, 0.25, 1)'CSS easing string.
stagger0Delay between lines in ms. 0 = all settle together.
activetrueSet false to skip animation entirely.
targetTracking0Extra letter-spacing (em) each line settles to, on top of the element's own. 0 = natural spacing. 'auto' measures per-line optical density and evens it out: dense lines settle slightly looser, sparse lines slightly tighter, within ±0.05em. A positive amount is limited to the room each line has, so the settled text never overflows.
direction'expand''expand' — each line starts looser (by up to spread) and settles in. 'compress' — each line starts tighter (by up to spread) and settles out.
intersectfalseWhen true, re-runs the animation each time the element scrolls into view.
quietReplayfalseWhen true, a replay keeps the existing lines and offsets each one from its settled spacing, then eases back (staggered when stagger is set, all at once when it is 0), instead of rebuilding the element.
lineDetection'bcr''bcr' reads actual browser layout — ground truth, works with any font and inline HTML. 'canvas' uses @chenglou/pretext for arithmetic line breaking with no forced reflow on resize. Install pretext separately.

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-typsettle. Configure it with data-* attributes.

<!-- Site Settings → Custom Code → Footer, or an Embed element -->
<script src="https://cdn.jsdelivr.net/npm/@overpunch/typsettle/dist/typsettle.webflow.min.js"></script>

<!-- Then add data-typsettle to any text element -->
<h1 data-typsettle>Your headline</h1>

Framer

Insert → Code → New Component, then paste Typsettle.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/@overpunch/typsettle"

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).