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.

perspective: 5000pxrotateX: 0.0°rotateY: 0.0°wght: 300tracking: 0.0000em

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 } from '@liiift-studio/stabiltype'

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

Hook — attach to any element

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

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.08],
  weightRange: [300, 600],
  perspective: 600,
  tilt: 4,
})

// 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

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 smoothing factor 0–1. Higher = more smoothing (slower response).
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.
as'p'HTML element to render. (StabilTypeText only)