Single-line text fitting! Variable fonts, ink-box bounds, height-aware fitting, and an algebraic solver that makes resize smooth.
new CoolTextFit().fit(element)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.
https://aannoo.github.io/cool-text-fit/
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 (dist/cool-text-fit.d.ts).
<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).
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.
| 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. |
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.
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.
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.
ResizeObserver and MutationObserver are wired up automatically. Resize the container or change the text and CoolTextFit refits, with configurable debouncing.
Detects when a web font finishes loading and refits all affected elements with correct metrics.
Respects text-transform on the element (uppercase, lowercase, capitalize). Text is measured using the transformed value so the fit matches what the browser renders.
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.
| 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. |
- 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.
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'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 },
})The pattern is the same everywhere: get a ref to the DOM element, call fit() after mount, call disconnect() on unmount.
React
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>
)
}Vue
<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>Svelte
<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>Solid
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).
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. disconnect()restores the element's original inlinewhite-space,height, andvisibilityvalues.- For
contenteditableelements, orphan text nodes are adopted back into the wrapper, direct childdivandpelements are flattened, and placeholderbrnodes are removed on each fit.
If you style the element's text directly, target the wrapper too:
.your-title,
.your-title .cool-text-fit-wrapper {
/* your typography */
}// Stop observers for one element
ctf.disconnect(element)
// Stop all observers
ctf.disconnectAll()
// Full teardown (removes internal DOM nodes, clears caches)
ctf.cleanup()- 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.
Works in all modern browsers. Canvas-based measurement is preferred where supported; DOM fallback handles edge cases automatically.
MIT
