Skip to content

Liiift-Studio/Typsettle

Repository files navigation

Typsettle

npm License: MIT part of liiift type-tools

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. Like watching a compositor tune a paragraph. Respects prefers-reduced-motion.

Each line of a paragraph starts at a random letter-spacing offset and eases independently to its settled tracking, staggered line by line.

Each line starts at a random tracking offset and eases to its settled spacing, staggered line by line. (live demo)

typsettle.com · npm · GitHub

TypeScript · Zero runtime dependencies (~4 kB gzip) · React + Vanilla JS


Install

npm install @liiift-studio/typsettle

Usage

Next.js App Router: this library uses browser APIs. Add "use client" to any component file that imports from it.

React component

'use client'

import { SettleText } from '@liiift-studio/typsettle'

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

React hook

'use client'

import { useSettle } from '@liiift-studio/typsettle'

// Inside a React component:
const { ref, replay } = useSettle({ spread: 0.04, duration: 800, stagger: 80 })
return <p ref={ref}>Your paragraph text here...</p>

The hook returns a replay function so you can re-run the settle on demand — here, a complete component with a replay button:

'use client'

import { useSettle } from '@liiift-studio/typsettle'

export function SettlingParagraph() {
  const { ref, replay } = useSettle({ spread: 0.04, duration: 800, stagger: 80 })
  return (
    <>
      <p ref={ref}>
        Paragraph text enters from randomised letter-spacing and eases to
        optical equilibrium.
      </p>
      <button onClick={replay}>Replay</button>
    </>
  )
}

Vanilla JS

import { applySettle, removeSettle, replaySettle, getCleanHTML } from '@liiift-studio/typsettle'

const el = document.querySelector('p')
const original = getCleanHTML(el)

applySettle(el, original, { spread: 0.04, duration: 800, stagger: 80 })

// Re-run after custom fonts load — line detection uses BCR, which gives wrong
// line groups if the font hasn't swapped in yet. applySettle resets to original first,
// so re-calling it is safe:
document.fonts.ready.then(() => {
  applySettle(el, original, { spread: 0.04, duration: 800, stagger: 80 })
})

// The line spans remain in the DOM after the animation completes.
// Call removeSettle to restore original markup (e.g. before re-running):
// removeSettle(el, original)

// Replay the settle animation on a previously-settled element:
// replaySettle(el)

TypeScript

import type { SettleOptions } from '@liiift-studio/typsettle'

const opts: SettleOptions = { spread: 0.04, duration: 800, stagger: 80, active: true }

SSR & Next.js

The animation is client-only by design — it reads live browser layout to detect line breaks, so it never runs on the server. Render your text normally (it ships as plain markup, fully indexable and accessible), and the settle wraps and animates it after mount:

  • No hydration mismatch. The server emits your original paragraph markup; line-wrapping and the random per-line offsets are applied client-side in a layout effect, after React has hydrated. There is nothing random in the server output to mismatch.
  • "use client" required. Any file importing from this package must be a Client Component in the App Router (the package touches window, requestAnimationFrame, and matchMedia).
  • Settled is the resting state. If JS never runs, or prefers-reduced-motion: reduce is set, the reader simply sees the paragraph at its natural spacing — the animation degrades to nothing, not to broken markup.

Options

Option Default Description
spread 0.04 Max initial letter-spacing offset in em. Each line gets a random value in [-spread, +spread]
duration 800 CSS transition duration in ms
easing 'cubic-bezier(0.25, 0.1, 0.25, 1)' CSS easing string
stagger 0 Delay between lines in ms. 0 settles all lines together; 80 gives a cascading effect
active true Set false to skip the animation entirely (e.g. for conditional disabling)
targetTracking 0 Letter-spacing each line settles to in em. 0 = natural spacing. 'auto' measures the original rendered tracking and settles there
direction 'expand' 'expand' animates from condensed → normal tracking; 'compress' animates from normal → condensed
intersect false Replay the animation each time the element scrolls into view
quietReplay false When true, replays with each line individually offsetting then settling (staggered), instead of all lines flashing simultaneously. Has no effect when stagger is 0
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 (npm install @chenglou/pretext). Falls back to 'bcr' while pretext loads
as 'p' HTML element to render, e.g. 'h1', 'div'. (React component only)

API reference

Core functions

Function Description
applySettle(element, originalHTML, options) Wrap lines and run the settle animation
removeSettle(element, originalHTML) Restore the element to its original markup
replaySettle(element) Replay the settle animation on a previously-settled element
getCleanHTML(element) Return the element's inner HTML with any Typsettle spans stripped

React hook

useSettle(options) returns { ref, replay }:

Key Type Description
ref React.RefObject Attach to the element you want to animate
replay () => void Call to replay the settle animation imperatively

SettleText component props

Accepts all SettleOptions plus:

Prop Type Description
as string HTML element to render (default 'p')
onReady (replay: () => void) => void Callback fired once the animation completes, receiving a replay function

How it works

Each visual line is wrapped in a <span>. A random letter-spacing value in [-spread, +spread] em is applied immediately. On the next requestAnimationFrame, a CSS transition is set on each span and letter-spacing is set to 0em — the browser animates each line back to zero. Stagger is implemented as a per-span transition-delay of i × stagger ms.

The line spans are not automatically removed after the transition completes — they remain in the DOM with letter-spacing: 0em. Call removeSettle(el, original) manually if you need to restore the original markup (e.g. before a re-run). The animation is skipped entirely if prefers-reduced-motion: reduce is set or active is false.

Line break safety: Line breaks are locked to the browser's natural layout. Each run starts from the original HTML, detects lines at zero letter-spacing, then wraps them with white-space: nowrap. Word breaks never change during or after the animation. Lines may overflow briefly during the transition (when random offsets are applied) but settle to letter-spacing: 0em — their exact natural width — by the time the animation ends.


Dev notes

next in root devDependencies

package.json at the repo root lists next as a devDependency. This is a Vercel detection workaround — not a real dependency of the npm package. Vercel's build system inspects the root package.json to detect the framework; without next present it falls back to a static build and skips the Next.js pipeline, breaking the /site subdirectory deploy.

The package itself has zero runtime dependencies. Do not remove this entry.


Future improvements

  • Variable font axis settle — settle wdth or wght instead of (or alongside) letter-spacing, for fonts where axis variation reads more clearly at large sizes
  • Random seed — accept a seed option for deterministic random offsets, so repeated runs and snapshot tests reproduce the same starting state (the offsets are applied client-side after hydration, so this is for reproducibility, not for resolving any SSR mismatch — see SSR & Next.js)

Current version: 1.0.20

About

Page-load animation where per-line tracking settles from random offsets to optical equilibrium

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors