opszstepper

Optical cuts,
on demand.

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

Type designers create separate optical-size cuts for the same reason optometrists prescribe different lenses for reading and driving — the geometry that works at 12px becomes wrong at 72px. Opsz Stepper detects the current font-size and hot-swaps to the correct cut, automatically.

Live demo — drag the sliders

Font Size32px
Hysteresis1px
MicroTextDisplay
Cormorant Garamond — text optical size

The geometry that works at twelve points becomes wrong at seventy-two. Type designers know this — it's why they draw separate optical-size cuts. Stroke widths, apertures, spacing: all redrawn for the intended size.

Micro cutCormorant SC< 16px
Text cutCormorant Garamond16px – 36px
Display cutCormorant Display≥ 36px

Drag the font-size slider to cross the 16px and 36px thresholds — watch the typeface change. The hysteresis slider sets the dead zone: font-size must overshoot the boundary by that many pixels before the cut switches, preventing oscillation at the edge.

How it works

Optical sizes are different drawings

Micro, Text, and Display variants of the same typeface aren't simply scaled versions of each other. They have different stroke widths, apertures, x-heights, and spacing — each redrawn from scratch to be optically correct at its intended size range.

ResizeObserver watches the element

When the element's computed font-size changes — because of responsive CSS, viewport units, or user zoom — the ResizeObserver fires. Opsz Stepper re-reads the font-size and re-evaluates which cut to apply, keeping the typeface optically correct at every size.

Hysteresis prevents oscillation

If font-size sits exactly at a cut boundary — say, precisely 16px — the element would flip between cuts on every resize event. The hysteresis dead zone prevents this: font-size must pass the boundary by N pixels before the cut switches, so the boundary feels stable rather than jittery.

Works with any font family

Cuts are just CSS font-family strings. Google Fonts, locally hosted @font-face declarations, cloud fonts, Adobe Fonts — anything you can name in CSS works as a cut. The tool makes no assumptions about the fonts themselves.

Usage

TypeScript + React · Vanilla JS

Drop-in component

import { OpszStepperText } from '@liiift-studio/opszstepper'

<OpszStepperText cuts={[
  { family: 'Halyard Micro, sans-serif', maxSize: 13 },
  { family: 'Halyard Text, sans-serif', minSize: 13, maxSize: 28 },
  { family: 'Halyard Display, sans-serif', minSize: 28 },
]}>
  Your text here
</OpszStepperText>

Hook — attach to any element

import { useOpszStepper } from '@liiift-studio/opszstepper'

const ref = useOpszStepper({ cuts })
<p ref={ref}>Your text</p>

Vanilla JS

import { startOpszStepper } from '@liiift-studio/opszstepper'

const el = document.querySelector('h1')
const stop = startOpszStepper(el, { cuts })
// Call stop() to disconnect and restore original fontFamily

Options

OptionDefaultDescription
cutsrequiredArray of OpszStepperCut objects, each with a family string and optional minSize/maxSize in px.
hysteresis1Dead zone in px at each cut boundary. Prevents oscillation when font-size sits exactly at a threshold.
onCutChangeCallback fired each time the active cut changes. Receives the new OpszStepperCut object.
as'p'HTML element to render. Accepts any valid React element type. (OpszStepperText only)