{"id":49654286,"url":"https://github.com/darkroomengineering/fitbox","last_synced_at":"2026-05-31T01:02:14.712Z","repository":{"id":352898429,"uuid":"1217127372","full_name":"darkroomengineering/fitbox","owner":"darkroomengineering","description":"Reflow-free text-to-box fitting for React, built on Pretext.","archived":false,"fork":false,"pushed_at":"2026-04-22T19:01:02.000Z","size":316,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T22:25:02.419Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://fitbox.darkroom.engineering","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/darkroomengineering.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-21T15:14:13.000Z","updated_at":"2026-05-07T00:40:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/darkroomengineering/fitbox","commit_stats":null,"previous_names":["darkroomengineering/fitbox"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/darkroomengineering/fitbox","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkroomengineering%2Ffitbox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkroomengineering%2Ffitbox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkroomengineering%2Ffitbox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkroomengineering%2Ffitbox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/darkroomengineering","download_url":"https://codeload.github.com/darkroomengineering/fitbox/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkroomengineering%2Ffitbox/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33715211,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-06T08:00:39.127Z","updated_at":"2026-05-31T01:02:14.704Z","avatar_url":"https://github.com/darkroomengineering.png","language":"TypeScript","funding_links":[],"categories":["Community Projects"],"sub_categories":["Live Demos"],"readme":"# fitbox\n\nReflow-free text-to-box fitting for React, built on [`@chenglou/pretext`](https://github.com/chenglou/pretext).\n\n---\n\n## Why this exists\n\nIf you have ever used [Fitty](https://github.com/rikschennink/fitty) or written your own \"shrink text to container\" logic, you know the shape of it: pick a font-size, put the element in the DOM, read `getBoundingClientRect()`, compare to the container, adjust, read again. The loop terminates quickly — five to ten iterations — but every read forces a layout reflow. Drag a window with twenty fittable headings on the page and you are asking the browser's layout engine to recompute itself thousands of times a second.\n\nEvery library does it this way because, until recently, the browser was the only oracle that could tell you how text would lay out. That changed when [Cheng Lou](https://github.com/chenglou) released [Pretext](https://github.com/chenglou/pretext), a text measurement and layout library that uses `canvas.measureText()` — which does *not* reflow — as ground truth. With per-glyph widths cached, measuring a wrapped paragraph is microseconds of arithmetic.\n\nfitbox is what a text-fitting library looks like when you build it on that primitive.\n\n### The math\n\nOnce measurement stops touching layout, the algorithm collapses.\n\n**Single-line fit is a closed form.** Text width scales linearly with font-size. Prepare the text once at `1px` and call its natural width `w₁`. For any container of width `W`:\n\n```\nfontSize = W / w₁\n```\n\nOne division. No search. No DOM.\n\n**Multi-line fit is a reflow-free binary search.** Line breaks are non-linear in `(fontSize, maxWidth)`, so we search — but there is a scaling invariant: `fontSize = s` at `maxWidth = W` wraps identically to `fontSize = 1` at `maxWidth = W / s`. So we prepare once at 1px and binary-search `s` by querying Pretext's `measureLineStats(handle, W / s)`. Ten iterations to pixel precision, still pure arithmetic.\n\n**Static fluid CSS.** Because single-line fit is linear in viewport width, the entire responsive curve is expressible as `clamp(min, calc(a + b·vw), max)` — a string the browser interpolates for free, zero runtime JavaScript. Fitty cannot produce this; it cannot know the fit without measuring the DOM. fitbox computes the clamp at build or load time and ships it inline.\n\n**SSR.** Pretext needs a canvas, not a DOM. Give it `@napi-rs/canvas` on the server, compute fits in a loader, serialize the result as a `preset`, hydrate with the correct font-size already rendered. No layout shift, ever.\n\n### What fitbox is and isn't\n\n| | Fitty | fitbox |\n|---|---|---|\n| Measurement | `getBoundingClientRect()` per probe | `canvas.measureText()`, cached |\n| Single-line fit | Binary search over DOM | `W / w₁` |\n| Multi-line fit | — | Reflow-free binary search |\n| Fluid CSS | Hand-rolled clamp | Computed `clamp(…)` |\n| SSR | — | Supported via canvas polyfill |\n| Bundle | ~4KB min+gz | ~1.3KB core / ~1.6KB react / ~1.7KB server (min+gz, each entry standalone) |\n\nfitbox is narrower than Fitty in one way — it ships a React adapter, not a plain-DOM binding — and wider in several others. Reach for Fitty if you need plain DOM or are supporting very old browsers. Reach for hand-rolled CSS fluid-typography recipes if you are comfortable guessing at your text's natural width. Reach for fitbox when you want the fit to be exact, to work under SSR, or to disappear into a static CSS string after the first render.\n\n### Beyond the DOM\n\nBecause measurement is reflow-free, nothing about the fit algorithm depends on the text ending up in an HTML element. `layoutFit` returns the per-line layout in a rendering-backend-agnostic shape:\n\n```ts\nimport { prepare, layoutFit } from '@darkroomengineering/fitbox';\n\nconst handle = prepare('Hello world', 'Inter');\nconst { fontSize, lines } = layoutFit(handle, { width: 1024, maxLines: 2 });\n// lines: Array\u003c{ text: string; width: number; y: number }\u003e\n```\n\nThose numbers feed directly into a WebGL/WebGPU text renderer (troika-three-text, drei's `\u003cText\u003e`, a custom SDF shader), an offscreen Canvas, an SVG generator, a PDF pipeline — anywhere you want typography with correct fit and no DOM.\n\n---\n\n## Install\n\n```sh\nbun add @darkroomengineering/fitbox\n```\n\n## `useFit` — drop a ref on any element\n\n```tsx\nimport { useFit } from '@darkroomengineering/fitbox/react';\n\n\u003ch1 ref={useFit()}\u003eHello\u003c/h1\u003e\n\u003cp ref={useFit({ maxLines: 3, maxSize: 48 })}\u003e{text}\u003c/p\u003e\n```\n\nThat's the whole API for the common case. The hook reads `textContent` and the element's computed `font-family`/`font-weight`/`font-style`, runs `prepare` once, then mutates `element.style.fontSize` directly — no React re-render per resize frame, no `style` prop to merge.\n\n- A `ResizeObserver` refits on container resize.\n- A `MutationObserver` refits when the text changes (so `\u003cp ref={useFit()}\u003e{dynamic}\u003c/p\u003e` works).\n- `document.fonts.ready` gates first measurement.\n- Requires React 19+ (uses the callback-ref cleanup pattern).\n\n## `\u003cFitText\u003e` — component sugar\n\nFor the cases where a component wrapper is tidier:\n\n```tsx\nimport { FitText } from '@darkroomengineering/fitbox/react';\n\n\u003cFitText maxLines={3} as=\"h1\"\u003e\n  Typography that actually fits its container.\n\u003c/FitText\u003e\n```\n\nInternally uses `useFit`. Also accepts `fluid={…}` for static-clamp CSS and `preset={…}` for SSR-shipped initial sizes.\n\n## `useFitText` — explicit text + React-styled\n\nEscape hatch when you want React to own the styling (CSS-in-JS composition, or you need the full `FitResult` for downstream logic):\n\n```tsx\nimport { useFitText } from '@darkroomengineering/fitbox/react';\n\nconst { ref, style, result } = useFitText\u003cHTMLHeadingElement\u003e(text, {\n  maxLines: 2,\n  maxSize: 120,\n});\nreturn \u003ch1 ref={ref} style={style}\u003e{text}\u003c/h1\u003e;\n```\n\n## Fluid CSS — no JS at runtime\n\nFor responsive single-line headings, emit a static `clamp()` and let the browser interpolate:\n\n```tsx\nimport { prepare, fluidFit } from '@darkroomengineering/fitbox';\nimport { FitText } from '@darkroomengineering/fitbox/react';\n\nconst fluid = fluidFit(prepare('Fitbox', 'Inter'), {\n  minViewport: 320,\n  maxViewport: 1440,\n  minSize: 24,\n  maxSize: 180,\n});\n// fluid.cssClamp === 'clamp(120.755px, calc(103.827px + 5.29vw), 180px)'\n// (bounds are derived: sMin/sMax = clamp(viewport / naturalWidth, minSize, maxSize),\n//  so for short text the floor often lands above your minSize — here 'Fitbox' is wide\n//  enough at 320px viewport that the lower bound is 120.755px, not 24px.)\n\n\u003cFitText fluid={fluid}\u003eFitbox\u003c/FitText\u003e\n```\n\nFor wrapping text, `fluidFitMultiLine` probes the viewport range, finds breakpoints where line count changes, and emits a stylesheet of media-query-scoped clamps.\n\n## SSR\n\n```ts\n// entry.server.ts\nimport { createCanvas } from '@napi-rs/canvas';\nimport { configureServerCanvas } from '@darkroomengineering/fitbox/server';\n\nconfigureServerCanvas(() =\u003e createCanvas(1, 1), { cacheMax: 1024 });\n```\n\n```ts\n// routes/home.ts — react-router loader\nimport { fitCached } from '@darkroomengineering/fitbox/server';\n\nexport async function loader() {\n  return {\n    title: fitCached('Hello', 'Inter', { width: 1200, maxLines: 1 }),\n  };\n}\n```\n\n```tsx\n// routes/home.tsx\nimport { FitText } from '@darkroomengineering/fitbox/react';\nimport { useLoaderData } from 'react-router';\n\nexport default function Home() {\n  const { title } = useLoaderData();\n  return \u003cFitText preset={title}\u003eHello\u003c/FitText\u003e;\n}\n```\n\n`fitCached` / `fluidFitCached` / `fluidFitMultiLineCached` memoize in bounded LRUs so repeated calls (nav labels, common strings) don't re-measure. The multi-line variant is the most expensive call — it runs ~33 binary-search fits per invocation — so caching matters most there.\n\n## API\n\n### `@darkroomengineering/fitbox`\n\n- `prepare(text, fontFamily, options?)` — build a 1px Pretext handle.\n- `fit(handle, { width, height?, maxLines?, minSize?, maxSize?, lineHeight? })` — closed-form single-line or binary-search multi-line.\n- `layoutFit(handle, fitOpts)` — same as `fit`, plus `lines: Array\u003c{ text, width, y }\u003e` for non-DOM renderers (WebGL, WebGPU, Canvas, SVG).\n- `fluidFit(handle, { minViewport, maxViewport, widthFraction?, minSize?, maxSize? })` — single-line CSS clamp.\n- `fluidFitMultiLine(handle, { …, maxLines, samples?, selector? })` — piecewise `@media` stylesheet for wrapping text.\n\n### `@darkroomengineering/fitbox/react`\n\n- `useFit(options?)` — callback ref. Fits `textContent` to container, mutates `style.fontSize` directly. The primary one-liner.\n- `useFitText\u003cE\u003e(text, options)` — returns `{ ref, style, result }`. Escape hatch when you want React to own styling or need the raw `FitResult`.\n- `\u003cFitText\u003e` — element wrapper. Accepts `as`, `preset`, `fluid`.\n\n### `@darkroomengineering/fitbox/server`\n\n- `configureServerCanvas(factory, options?)` — install canvas shim, configure cache.\n- `fitCached(text, family, fitOpts, prepareOpts?)` — cached `prepare + fit`.\n- `fluidFitCached(text, family, fluidOpts, prepareOpts?)` — cached `prepare + fluidFit`.\n- `fluidFitMultiLineCached(text, family, fluidOpts, prepareOpts?)` — cached `prepare + fluidFitMultiLine`.\n- `clearServerCache()`.\n\n## Caveats\n\n- Pretext uses `canvas.measureText()` as ground truth. On SSR you need `@napi-rs/canvas` or similar and `configureServerCanvas()` called once at startup.\n- `fluidFitMultiLine` interpolates linearly within stable-line-count segments; wrapping shifts inside a segment cause minor imprecision. Increase `samples` to narrow.\n- The server cache is an LRU by `JSON.stringify` of inputs — fine for curated strings, not suited for unbounded user content without the `cacheMax` cap.\n\n## Acknowledgments\n\n- [Rik Schennink](https://github.com/rikschennink) — Fitty, the canonical fit-text-to-box library and the shape of this problem.\n- [Cheng Lou](https://github.com/chenglou) — Pretext, the reflow-free measurement primitive this is built on.\n\n## License\n\nMIT — darkroom.engineering\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarkroomengineering%2Ffitbox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdarkroomengineering%2Ffitbox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarkroomengineering%2Ffitbox/lists"}