npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cool-text-fit

v1.0.0

Published

Smart text fitting with variable fonts, ink-box bounds, and algebraic solving

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-fit

Or 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

  1. Sample — Measure text at three font sizes (min, mid, max) to establish a width-to-fontSize ratio (k).
  2. Interpolate — Estimate the optimal font size from the ratio and container width (plus height, in balanced/height modes).
  3. Refine — Binary search in a ±5 px window around the estimate.
  4. Optimise — For the winning font size, solve fontWidthletterSpacingscaleX algebraically, each closing the remaining gap.
  5. Cache — Store the k model. 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: nowrap on the element.
  • Wraps content in a span.cool-text-fit-wrapper (display: inline-block).
  • Applies font-size, font-variation-settings (merging in wdth), letter-spacing, text-indent + padding-left (letter-spacing compensation), text-align, transform (translateX/Y + scaleX), and transform-origin to the wrapper.
  • In ink-box mode, also sets height on the element and margin-left/margin-right on the wrapper for glyph-tight horizontal trimming, and applies translateY for 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: none or has zero width/height, fitting will fail.
  • Variable font required for wdth. If the font doesn't support wdth, CoolTextFit operates as if fontWidth is { 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