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/coreimport { 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.
| 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 thesvelteexport condition (plain-JS component; the consuming app's Svelte plugin compiles it, no dependency preprocessing).@typefitter/react/@typefitter/vue— compiled todistwithtsc, the way React and Vue component libraries conventionally ship. Vue's is authored as a render function, so building the package needs no.vuetoolchain.
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.
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.
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 consumernpm 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 8137Verified 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 previewVerified 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 previewVerified 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/*.htmlVerified: 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.
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.
MIT — see LICENSE.