stabiltype

Motion adapts
your type.

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

On smart glasses, head motion creates perceptual blur on anchored text. CSS has no motion context — it can’t adjust tracking or weight in response to velocity. stabilType bridges that gap, interpolating letter‑spacing, wght, and opsz in real time as velocity changes.

Live demo — scroll this page, or use cursor / gyro

↕↔ Scroll this page to feel the effect

Typography has always assumed a fixed observer. The reader is still, the page is still, and every spacing decision optimises for that arrangement. But text increasingly appears in motion — on phones jostled by footsteps, on smart glasses updating while the wearer turns, on dashboards alive with road vibration.

stabilType acknowledges the moving reader. Scroll this page to feel the perspective compress and the text plane tilt in the direction of travel. Move diagonally and both axes respond simultaneously. Everything decays the moment motion stops — a fleeting impression of the force applied, then stillness.

How it works

Velocity in two dimensions

stabilType tracks both vertical and horizontal scroll velocity, each as a signed value — downscroll and rightscroll are positive; upscroll and leftscroll are negative. The combined magnitude drives font adaptation. The sign drives direction-specific tilt and lean.

Perspective compression + directional tilt

As speed increases, a CSS perspective() function tightens — the text plane compresses toward the viewer, like a dolly zoom. Simultaneously, rotateX and rotateY tilt the element in the direction of travel. Both effects decay the moment scrolling stops.

EMA smoothing prevents jitter

Raw velocity signals are noisy. stabilType applies an exponential moving average before mapping velocity to type properties — so the typography transitions feel deliberate rather than jittery. The smoothing factor is tunable per element.

Runs in a rAF loop or on demand

startStabilType wires up a requestAnimationFrame loop and accepts a getVelocity callback — connect your platform’s IMU or head‑tracking API directly. applyStabilType lets you drive it frame‑by‑frame from your own loop.

Usage

TypeScript + React · Vanilla JS

Drop-in component

import { StabilTypeText, useScrollVelocity } from '@liiift-studio/stabiltype'

// useScrollVelocity returns a Velocity2D ref updated each frame
const velocity = useScrollVelocity()

<StabilTypeText velocity={velocity}>
  Heading text
</StabilTypeText>

Hook — attach to any element

import { useStabilType, useScrollVelocity } from '@liiift-studio/stabiltype'
import { useRef } from 'react'

// useScrollVelocity returns a Velocity2D ref ({ x, y }) updated each frame
const velocity = useScrollVelocity()
const ref = useRef(null)
useStabilType(ref, velocity, { weightRange: [300, 700] })
<h1 ref={ref}>Heading text</h1>

rAF loop — vanilla JS with IMU callback

import { startStabilType } from '@liiift-studio/stabiltype'

const el = document.querySelector('h1')

// Built-in scroll listener — tracks both X and Y axes automatically
const stop = startStabilType(el, {
  trackingRange: [0, 0.06], // default
  weightRange: [300, 600],
  perspective: 600,
  tilt: 3, // default
})

// External 2D velocity source — e.g. IMU or pointer
const stop = startStabilType(el, () => ({ x: imu.vx, y: imu.vy }), options)

stop() // cancel loop and restore styles

Options

StabilTypeOptions reference
OptionDefaultDescription
trackingRange[0, 0.06]Letter-spacing range in em: [at rest, at max velocity].
weightRange[300, 600]wght axis range: [at rest, at max velocity].
opszRange[12, 24]opsz axis range: [at rest, at max velocity].
opacityRange[1, 0.7]Opacity range: [at rest, at max velocity].
smoothing0.15EMA weight of the new sample per frame, 0–1. Higher = faster response (less lag); 1 snaps instantly; 0 freezes.
velocityMax15Scroll velocity in px/frame that maps to maximum typography adjustment. Only used by startStabilType in built-in scroll mode — applyStabilType and useStabilType expect a pre-normalised –1…+1 velocity and ignore this option.
weightAxis'wght'Variable font weight axis tag.
opszAxis'opsz'Variable font optical size axis tag.
perspective600CSS perspective depth in px at peak velocity. Tighter = more dramatic compression. 0 to disable.
tilt3rotateX/rotateY in degrees at peak velocity. Sign follows scroll direction.
slntRange[8, -8]slnt axis range: [at peak upscroll, at peak downscroll]. No-op on fonts without a slnt axis.
slntAxis'slnt'Variable font slant axis tag.
liveBaseFVSfalseWhen false (default), font-variation-settings is read from the computed cascade once on activation and cached — eliminating a getComputedStyle() call per frame. Set to true only if external CSS dynamically changes font-variation-settings on the element after stabilType has started.
as'p'HTML element to render. (StabilTypeText only)

Accessibility

stabilType respects prefers-reduced-motion — when the user has requested reduced motion, startStabilType returns without installing the scroll listener or rAF loop, leaving the element unmodified.