cool-text-fit
v1.0.0
Published
Smart text fitting with variable fonts, ink-box bounds, and algebraic solving
Maintainers
Readme
CoolTextFit
Single-line text fitting! Variable fonts, ink-box bounds, height-aware fitting, and an algebraic solver that makes resize smooth.
new CoolTextFit().fit(element)Why CoolTextFit?
Other text-fitting libraries loop through font sizes until the text fits. CoolTextFit doesn't.
Instead of brute-forcing font-size, it uses a 4-lever optimization hierarchy: fontSize, then fontWidth (the variable font wdth axis), then letterSpacing, then scaleX as a last resort. Text compresses and expands through the font's own design, not geometric distortion.
A model-cached algebraic solver means the first fit takes ~7-15 measurements, and every subsequent refit (window resize, content change) uses math and one verification measurement. No layout thrashing.
8 KB gzipped. Zero dependencies.
Demo
https://aannoo.github.io/cool-text-fit/
Installation
npm install cool-text-fitOr via CDN:
<script src="https://unpkg.com/cool-text-fit"></script>
<script>
const { CoolTextFit: CTF } = CoolTextFit
const ctf = new CTF()
ctf.fit(document.querySelector('#title'))
</script>TypeScript declarations are included (src/index.d.ts).
Quick start
<div class="card">
<div id="title" style="font-family: InterVariable, system-ui; font-weight: 700;">
Variable width axis fit
</div>
</div>import { CoolTextFit } from 'cool-text-fit'
const ctf = new CoolTextFit()
ctf.fit(document.querySelector('#title'))That single call auto-detects your font's variable width range, picks the right measurement strategy, infers alignment from layout, sets up resize/mutation observers, and handles font loading.
CoolTextFit fits the element's text into its parent element's content box (padding and border are excluded).
Features
Variable font width
Most libraries only touch font-size. CoolTextFit manipulates the wdth variation axis so text narrows or widens natively, not the squished-text look you get from transform: scaleX(). It auto-detects the font's supported width range.
Three fitting modes
| Mode | Behavior |
|------|----------|
| width | Maximise font size to fill container width. Height may overflow. |
| height | Maximise font size to fill container height. Width may overflow. |
| balanced | Largest font size that fits both width and height. |
Ink-box measurement
textBounds: 'ink-box' measures actual glyph bounds (actualBoundingBoxAscent/Descent), not the line box. Text fills the container edge-to-edge without phantom whitespace above caps or below the baseline.
Canvas/DOM strategy switching
Feature-detects whether Canvas can handle variable fonts and letter-spacing for the current browser + font combination. Falls back to DOM measurement only when necessary. No browser sniffing.
O(1)-ish performance
Three-point interpolation seeds a narrow binary search. Results are cached as a linear model (k = width / fontSize), so refits resolve algebraically with a single verification measurement.
Auto observers
ResizeObserver and MutationObserver are wired up automatically. Resize the container or change the text and CoolTextFit refits, with configurable debouncing.
Font loading
Detects when a web font finishes loading and refits all affected elements with correct metrics.
CSS text-transform
Respects text-transform on the element (uppercase, lowercase, capitalize). Text is measured using the transformed value so the fit matches what the browser renders.
Configuration
Pass options to the constructor (instance-wide defaults) or to .fit() (per-element overrides):
const ctf = new CoolTextFit({
mode: 'balanced', // 'width' | 'height' | 'balanced'
textBounds: 'line-box', // 'line-box' | 'ink-box'
fontSize: { min: 14, max: 200 },
scaleX: { min: 0.5, max: 2 },
letterSpacing: { max: 60 },
fontWidth: 'auto', // 'auto' | { min: 75, max: 125 }
alignment: 'auto', // 'auto' | 'left' | 'center' | 'right'
observe: true, // attach resize/mutation observers
debounceMs: 0, // observer debounce (0 = next rAF)
waitForFonts: false // delay first fit until font loads
})
// Per-element override
ctf.fit(el, { fontSize: { max: 120 }, mode: 'width' })directFit() returns a FitResult object with the computed values (fontSize, fontWidth, scaleX, letterSpacing, finalWidth, finalHeight, fits, error). fit() does not return a result.
Options reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| mode | string | 'balanced' | Fitting constraint mode. |
| textBounds | string | 'line-box' | Use 'ink-box' for glyph-tight bounds. |
| fontSize | { min, max } | { min: 14, max: 200 } | Font size search range in px. |
| scaleX | { min, max } | { min: 0.5, max: 2 } | Horizontal scale limits. |
| letterSpacing | { max } | { max: 60 } | Maximum letter spacing in px. Minimum is always 0. |
| fontWidth | 'auto' | { min, max } | 'auto' | Variable font wdth axis range. 'auto' probes the font. |
| alignment | string | 'auto' | Transform origin alignment. 'auto' infers from layout. |
| observe | boolean | true | Auto-refit on resize/content change. |
| debounceMs | number | 0 | Debounce delay for observer-triggered refits. |
| waitForFonts | boolean | false | Wait for font load before first fit. |
How it works
- Sample — Measure text at three font sizes (min, mid, max) to establish a width-to-fontSize ratio (
k). - Interpolate — Estimate the optimal font size from the ratio and container width (plus height, in
balanced/heightmodes). - Refine — Binary search in a ±5 px window around the estimate.
- Optimise — For the winning font size, solve
fontWidth→letterSpacing→scaleXalgebraically, each closing the remaining gap. - Cache — Store the
kmodel. On refit, skip to step 4 with one measurement to verify.
Architecture
Three layers, so you can use exactly as much as you need:
CoolTextFit ← Full auto: observers, font detection, alignment inference
└─ CoolTextFitBase ← Manual API: explicit config, no observers
└─ CoolTextFitCore ← Pure math: zero DOM, portable to workers/Node// Layer 1 — Pure algorithm (no DOM)
import { CoolTextFitCore } from 'cool-text-fit'
// Layer 2 — Manual control (requires explicit fontWidth + alignment)
import { CoolTextFitBase } from 'cool-text-fit'
// Layer 3 — Full auto (default)
import { CoolTextFit } from 'cool-text-fit'
// Default export (same as CoolTextFit)
import CoolTextFit from 'cool-text-fit'Using CoolTextFitBase
CoolTextFitBase requires explicit fontWidth and alignment, no "auto".
init() triggers font loading via document.fonts.load() and waits for the font to be ready. Call it before directFit() if your font hasn't loaded yet. It accepts an array of { element } objects and deduplicates by font family.
import { CoolTextFitBase } from 'cool-text-fit'
const base = new CoolTextFitBase()
// Optional: pre-load fonts before fitting
await base.init([{ element: el }])
base.directFit(el, {
fontWidth: { min: 75, max: 125 },
alignment: 'center',
mode: 'balanced',
textBounds: 'line-box',
fontSize: { min: 12, max: 120 },
scaleX: { min: 0.8, max: 1.2 },
letterSpacing: { max: 10 },
})Framework usage
The pattern is the same everywhere: get a ref to the DOM element, call fit() after mount, call disconnect() on unmount.
import { useRef, useEffect } from 'react'
import { CoolTextFit } from 'cool-text-fit'
const ctf = new CoolTextFit()
function FitTitle({ text }) {
const ref = useRef(null)
useEffect(() => {
ctf.fit(ref.current)
return () => ctf.disconnect(ref.current)
}, [text])
return (
<div className="card">
<div ref={ref}>{text}</div>
</div>
)
}<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { CoolTextFit } from 'cool-text-fit'
const el = ref(null)
const ctf = new CoolTextFit()
onMounted(() => ctf.fit(el.value))
onBeforeUnmount(() => ctf.disconnect(el.value))
</script>
<template>
<div class="card">
<div ref="el">{{ text }}</div>
</div>
</template><script>
import { onMount } from 'svelte'
import { CoolTextFit } from 'cool-text-fit'
let el
const ctf = new CoolTextFit()
onMount(() => {
ctf.fit(el)
return () => ctf.disconnect(el)
})
</script>
<div class="card">
<div bind:this={el}>{text}</div>
</div>import { onMount, onCleanup } from 'solid-js'
import { CoolTextFit } from 'cool-text-fit'
const ctf = new CoolTextFit()
function FitTitle(props) {
let el
onMount(() => ctf.fit(el))
onCleanup(() => ctf.disconnect(el))
return (
<div class="card">
<div ref={el}>{props.text}</div>
</div>
)
}CoolTextFit's built-in MutationObserver handles text changes automatically, so you don't need to refit manually when props update (unless observe: false).
What CoolTextFit does to the DOM
CoolTextFit applies several changes to your element:
- Sets
white-space: nowrapon the element. - Wraps content in a
span.cool-text-fit-wrapper(display: inline-block). - Applies
font-size,font-variation-settings(merging inwdth),letter-spacing,text-indent+padding-left(letter-spacing compensation),text-align,transform(translateX/Y+scaleX), andtransform-originto the wrapper. - In
ink-boxmode, also setsheighton the element andmargin-left/margin-righton the wrapper for glyph-tight horizontal trimming, and appliestranslateYfor vertical centering.
If you style the element's text directly, target the wrapper too:
.your-title,
.your-title .cool-text-fit-wrapper {
/* your typography */
}Cleanup
// Stop observers for one element
ctf.disconnect(element)
// Stop all observers
ctf.disconnectAll()
// Full teardown (removes internal DOM nodes, clears caches)
ctf.cleanup()Gotchas
- Parent must have a real size. If the parent container is
display: noneor has zero width/height, fitting will fail. - Variable font required for
wdth. If the font doesn't supportwdth, CoolTextFit operates as iffontWidthis{ min: 100, max: 100 }. - Ink-box is tighter. Use
'ink-box'for edge-to-edge visual alignment; use'line-box'for standard typographic line behavior.
Browser support
Works in all modern browsers. Canvas-based measurement is preferred where supported; DOM fallback handles edge cases automatically.
License
MIT
