stabiltype
Motion adapts
your type.
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 stylesOptions
| Option | Default | Description |
|---|---|---|
| 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]. |
| smoothing | 0.15 | EMA weight of the new sample per frame, 0–1. Higher = faster response (less lag); 1 snaps instantly; 0 freezes. |
| velocityMax | 15 | Scroll 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. |
| perspective | 600 | CSS perspective depth in px at peak velocity. Tighter = more dramatic compression. 0 to disable. |
| tilt | 3 | rotateX/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. |
| liveBaseFVS | false | When 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.