Skip to content

Repository files navigation

typefitter

Framework-agnostic wall-to-wall text fitting — size a single line of type so its ink spans its container edge-to-edge.

Measures a given typeface's advance width, reduces it to a ratio, and allows CSS cqw to scale it.

No search loop, no resize listener, no re-measurement. The width information is recorded once per font so that CSS can do the rest.

npm i @typefitter/core
import { measureFromDOM, whenFontsReady } from '@typefitter/core/dom';
import { fitToStyle, styleString } from '@typefitter/core';
import '@typefitter/core/css'; // once, app-wide — the constant render rules

const face = { family: 'Inter', weight: 700, style: 'normal' };
await whenFontsReady(["700 100px 'Inter'"]); // measuring before the face loads caches a wrong ratio
const style = styleString(fitToStyle(measureFromDOM('Wall to wall', face), face));
// → <div class="fit"><span class="fit__text" style="…">Wall to wall</span></div>

Using React, Vue, or Svelte? The <FitText> adapters below wrap all of that in a component. Building static or server-rendered pages? Measure from font bytes at build with @typefitter/core/node and ship zero client JS — see the two consumption paths.

Packages

Package What it is Runs where
@typefitter/core The Fit shape, the math, fitToStyle, and fit.css. isomorphic
@typefitter/core/node measureFromFont — build/SSR measurement from font bytes (fontkit). Node
@typefitter/core/dom measureFromDOM + whenFontsReady — client measurement. browser
@typefitter/catalog Optional multi-cut selection + nearest-cut substitution, generic over any family. isomorphic
@typefitter/svelte Svelte 5 <FitText> component over the client + build measurers. browser (Svelte)
@typefitter/react React <FitText> component over the client + build measurers. browser (React)
@typefitter/vue Vue 3 <FitText> component over the client + build measurers. browser (Vue)

The core is framework-free. Each adapter is a thin wrapper over @typefitter/core/dom + fitToStyle, exposing the same <FitText> surface (props text / face / optional precomputed fit / tracking / transform / flush / maxSize / leading / color / underline, plus a fit-diagnostics callback). They differ only in how each ecosystem ships a component:

  • @typefitter/svelte — source, via the svelte export condition (plain-JS component; the consuming app's Svelte plugin compiles it, no dependency preprocessing).
  • @typefitter/react / @typefitter/vue — compiled to dist with tsc, the way React and Vue component libraries conventionally ship. Vue's is authored as a render function, so building the package needs no .vue toolchain.

One reactivity note the adapters encode identically: the diagnostics callback must depend only on the fit and the element — never auto-track everything it reads — or emitting a result re-enters the framework's update cycle and loops. Svelte's $effect(el, f) and React's useEffect([f]) get this for free; the Vue adapter uses an explicit watch([f, el]) rather than a watchEffect for the same reason.

The two consumption paths

Build-time / SSR (zero client JS). Any stack with a build or server render step — Next, Astro, Nuxt, SvelteKit, 11ty, or a bare script — calls measureFromFont and inlines the result. Correct on first paint, works with JS off.

Client. For interactive / CMS copy, call whenFontsReady() then measureFromDOM. Same Fit, minus per-cut vertical metrics.

Run the example

npm install
npm run build            # tsc: core → catalog → react → vue
npm test                 # vitest: the math, node measurement, catalog (no build needed)
npm run typecheck        # tsc / vue-tsc --noEmit across packages + example apps
npm run consume          # builds core + catalog, runs the build-time consumer

npm test covers the pure math and the Node measurement path (against a real font binary — one the OS already has, so no font files live in this repo); the DOM/canvas path needs a real browser, which the example apps verify in-browser (see below).

examples/build-consumer measures its copy against real font bytes — the installed Arial family, read straight off the OS — and writes dist/index.html: a static, zero-JS page that ships no font files and needs no @font-face, because the browser already has those faces. Serve it:

cd examples/build-consumer/dist && python3 -m http.server 8137

Verified in-browser: text ink spans the container to < 0.06px at every width, with no re-measurement on resize.

examples/svelte-app is a Vite + Svelte 5 SPA using @typefitter/svelte's <FitText> on the client path across a range of system fonts (no font files):

cd examples/svelte-app && npx vite build && npx vite preview

Verified in-browser across 11 faces (serif / sans / mono / italic / Impact / Comic Sans): every line fits to < 0.05px, and holds sub-pixel when the width is dragged — measured once, scaled by CSS.

examples/react-app (React 19 + @typefitter/react) and examples/vue-app (Vue 3 + @typefitter/vue) are the same client-path demo — the identical 11-face gallery, width slider, caps toggle, and live edge-error badge — so the three adapters can be compared side by side:

cd examples/react-app && npx vite build && npx vite preview
cd examples/vue-app   && npx vite build && npx vite preview

Verified in a real browser: both fit every face to < 0.06px at full width and stay sub-pixel when the stage is narrowed (React 0.019px, Vue 0.042px at ~40%), with a clean console — measured once, scaled by CSS.

examples/ssr-consumer is the build-path counterpart for React and Vue (the role sveltekit-app's prerender band plays for Svelte), without a framework scaffold. It measures system-font bytes at build and renders the same line through both <FitText> components — React via react-dom/server, Vue via @vue/server-renderer — passing the precomputed fit, then writes static, zero-JS react.html / vue.html. It also asserts the two adapters agree: same fit in → same per-element style out.

npm run consume:ssr      # build + render both → examples/ssr-consumer/dist/*.html

Verified: 4/4 lines render identically across the two adapters; the static pages carry zero <script> tags and fit every line to < 0.06px on first paint.

examples/sveltekit-app shows both paths on one page, using random system fonts and generic copy. The prerendered band measures 4 randomly-chosen system fonts with measureFromFont — reading their OS font files (.ttf) at build — and inlines the font-sizes into static HTML (zero client JS, fontkit absent from the client bundle). Below it, a live input is fitted client-side with measureFromDOM. Re-run the build to reshuffle the fonts. A sticky width bar resizes both stages to show the type reflow responsively — with a live badge proving it stays sub-pixel at every width (measured once, scaled by CSS).

A CAPS / As-typed selector shows a subtlety of the build path: uppercase and as-typed have different advance widths, so the prerendered band precomputes both fits at build and swaps between them (staying zero-JS-correct) rather than flipping text-transform at runtime; the client band just re-measures.

cd examples/sveltekit-app && npx vite build && npx vite preview   # serves build/

Verified: hero lines carry --fit-ratio inline in the prerendered HTML and fit to < 0.07px on first paint (including cap-less faces like Impact, which correctly omit the Firefox --fb-* trim); the client input re-fits sub-pixel on every keystroke. Font-file paths are macOS-specific — the approach is portable, the font source is the consumer's.

What stays consumer-owned

Font bytes + @font-face loading, the specific axis catalog (e.g. examples/build-consumer/system-catalog.mjs), and the SSG/SSR moment that calls measureFromFont. The library ships the function and the CSS; the stack supplies the build step.

This repo contains no font binaries.gitignore blocks them, so no licensed face is ever redistributed by accident. The examples and tests read the fonts already installed on the machine instead.

Licence

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages