npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@liiift-studio/opszstepper

v1.0.11

Published

Multi-cut optical family hot-swap by font-size — automatically applies Micro, Text, or Display cuts based on the element's computed font-size

Readme

opszStepper

npm License: MIT part of liiift type-tools

font-optical-sizing: auto only works for variable fonts with an opsz axis. opszStepper solves the other case: professional typeface families that ship separate font files for each optical size cut (Micro, Text, Display) with no axis at all. It automatically swaps the correct cut onto an element as its font-size changes.

opszstepper.com · npm · GitHub

TypeScript · Zero dependencies · React + Vanilla JS


Install

npm install @liiift-studio/opszstepper

Usage

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

What are optical cuts?

Many professional editorial typefaces ship as a family of separate font files — each drawn specifically for a different size range. Halyard has Halyard Micro (captions and footnotes), Halyard Text (body), and Halyard Display (headlines). Tiempos has Tiempos Fine, Tiempos Text, and Tiempos Headline. Each cut has different contrast, spacing, and stroke weight tuned for its intended size. CSS has no mechanism to switch between them automatically — font-optical-sizing: auto only controls the opsz axis of a single variable font file. opszStepper fills that gap.

React 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 paragraph text here...
</OpszStepperText>

React hook

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

// Inside a React component:
const ref = useOpszStepper({
  cuts: [
    { family: 'Halyard Micro, sans-serif', maxSize: 13 },
    { family: 'Halyard Text, sans-serif', minSize: 13, maxSize: 28 },
    { family: 'Halyard Display, sans-serif', minSize: 28 },
  ],
})
return <p ref={ref}>{children}</p>

The hook starts a ResizeObserver on the element and re-evaluates the active cut each time the element's size changes (which triggers a re-read of font-size). It restarts automatically when cuts.length or hysteresis changes, and cleans up on unmount.

Vanilla JS — with ResizeObserver

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

const el = document.querySelector('p')

const cuts = [
  { family: 'Halyard Micro, sans-serif', maxSize: 13 },
  { family: 'Halyard Text, sans-serif', minSize: 13, maxSize: 28 },
  { family: 'Halyard Display, sans-serif', minSize: 28 },
]

let stop = startOpszStepper(el, { cuts })

// Later — stop the observer and restore original fontFamily:
// stop()

Vanilla JS — one-shot

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

const el = document.querySelector('p')

applyOpszStepper(el, {
  cuts: [
    { family: 'Halyard Micro, sans-serif', maxSize: 13 },
    { family: 'Halyard Text, sans-serif', minSize: 13, maxSize: 28 },
    { family: 'Halyard Display, sans-serif', minSize: 28 },
  ],
})

// Later — restore original fontFamily:
// removeOpszStepper(el)

TypeScript

import type { OpszStepperCut, OpszStepperOptions } from '@liiift-studio/opszstepper'

const cuts: OpszStepperCut[] = [
  { family: 'Tiempos Fine, serif', maxSize: 13 },
  { family: 'Tiempos Text, serif', minSize: 13, maxSize: 28 },
  { family: 'Tiempos Headline, serif', minSize: 28 },
]

const opts: OpszStepperOptions = { cuts, hysteresis: 2 }

Options

| Option | Default | Description | |--------|---------|-------------| | cuts | (required) | Array of OpszStepperCut objects defining each optical size cut and the font-size range it applies to. Each cut has a family string (CSS font-family value), an optional minSize in px (inclusive, default 0), and an optional maxSize in px (exclusive, default Infinity). Ranges should be contiguous and non-overlapping — see the cuts configuration guide below | | hysteresis | 1 | Dead zone in px around each cut boundary. When font-size sits within hysteresis px of a threshold, the current cut is held rather than switching. Prevents oscillation when font-size is computed to hover right at a boundary due to sub-pixel rendering or responsive scaling. Increase to 24 if you observe rapid toggling | | onCutChange | undefined | Callback fired each time the active cut changes. Receives the newly applied OpszStepperCut. Useful for logging, analytics, or synchronising sibling elements | | as | 'p' | HTML element to render. Accepts any valid React element type, e.g. 'h1', 'div', 'span'. (React component only) |


Cuts configuration guide

A cut is active when the element's computed font-size satisfies minSize <= fontSize < maxSize. The bounds are in CSS pixels as returned by getComputedStyle(el).fontSize.

Structure cuts as contiguous ranges — each maxSize should equal the next cut's minSize:

cuts: [
  { family: 'Halyard Micro, sans-serif',   maxSize: 13       },  // 0px – 13px
  { family: 'Halyard Text, sans-serif',    minSize: 13, maxSize: 28 },  // 13px – 28px
  { family: 'Halyard Display, sans-serif', minSize: 28       },  // 28px – ∞
]

You can omit the smallest cut's minSize (defaults to 0) and the largest cut's maxSize (defaults to Infinity). If font-size falls outside all defined ranges — which should not happen with a complete contiguous set — the active cut is left unchanged.

Hysteresis at boundaries: if font-size is 13.4px and the active cut was Halyard Text (minSize: 13), the tool will not switch to Halyard Micro until fontSize < 13 - hysteresis (i.e. < 12 with the default hysteresis: 1). Moving in the other direction — Text to Display — requires fontSize > 28 + hysteresis (i.e. > 29). This prevents flicker when a responsive layout computes font-size to a value that oscillates across a boundary.


How it works

startOpszStepper reads the element's computed font-size via getComputedStyle(el).fontSize and finds the matching cut. It then sets el.style.fontFamily to that cut's family string, overriding whatever the stylesheet specifies. The original fontFamily value is stored in a WeakMap keyed by element so it can be restored exactly when removeOpszStepper or the stop function is called.

A ResizeObserver watches the element for size changes. In responsive layouts, font-size is typically driven by clamp(), viewport units, or container queries — all of which can change as the element or viewport resizes. Each observer callback re-reads font-size and applies hysteresis logic before switching cuts, so a cut swap only fires when the size has moved clearly past a threshold.

document.fonts.load() is not awaited. The cut swap is immediate — opszStepper sets font-family and the browser handles the font load. If a cut's font file has not yet loaded, the browser will show a fallback until it arrives (standard FOUT behaviour). If you need to eliminate FOUT, preload each cut's font file in the document <head> using <link rel="preload" as="font">. opszStepper does not manage font loading.

Original fontFamily is saved and restored. When removeOpszStepper(el) or the stop function from startOpszStepper is called, the element's style.fontFamily is reset to exactly the value it had before the first call. If the element had no inline fontFamily, it is restored to an empty string (clearing the inline property, deferring to the stylesheet).


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

  • Container query support — re-evaluate cuts in response to @container size changes, not just element resize, for components embedded in container-query layouts where font-size is driven by container width
  • Smooth crossfade — optionally apply a short CSS transition: font-family equivalent using a brief opacity fade between cuts to soften the swap on large editorial pages
  • Multi-element sync — a syncGroup option to tie multiple elements to the same active cut, so a heading and its pull-quote always use the same optical cut at all times
  • Font preload hints — automatically inject <link rel="preload"> tags for all cut font files on first call, so the browser can fetch them before they are needed
  • SSR hydration — detect the correct cut server-side via a CSS custom property or data attribute so the initial render uses the right font-family without a post-hydration swap

Current version: v1.0.10