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

modern-text

v2.2.2

Published

Measure and render text in a way that describes the DOM.

Readme

modern-text measures and renders rich text on Canvas with a layout model that mirrors the browser's. It has no React/Vue dependency, ships ESM + CJS, and is fully DOM-free — the same measure → render pipeline runs in the browser, Node, SSR or Web Workers.

Features

  • 📐 DOM-accurate layout — paragraphs, line wrapping, baselines, alignment.
  • 🧩 Pure-JS, DOM-free layout — the built-in Measurer computes layout from modern-font glyph metrics + kerning with no document, so it works in Node / SSR / Workers, is deterministic, and measures the exact font it renders.
  • ↔️ Horizontal & vertical writing modes (horizontal-tb, vertical-rl).
  • 🅰️ Rich inline styling — per-fragment font size/family/weight/style, color, letter-spacing, line-height, text-indent, text-align, vertical-align, text-decoration (underline / line-through / overline), text-transform, text-stroke, padding / margin.
  • 🎨 Fills & strokes — solid colors and linear gradients per fragment.
  • 🖍️ Highlights — draw an image/SVG behind selected fragments.
  • 🔵 List markersdisc / none / custom image bullets.
  • 🌑 Effects — stacked translate / skew / color layers (shadows, offsets).
  • 🌀 Text deformation — 34 opt-in presets (arch, bend, wave, trapezoid, ellipse, heart, …).
  • ✏️ <text-editor> web component — cursor, selection and keyboard editing.

Install

npm i modern-text modern-font

modern-font provides the font parsing/loading used for measuring and drawing glyphs.

Quick start

import { fonts } from 'modern-font'
import { renderText } from 'modern-text'

await fonts.loadFallbackFont('/fallback.woff')

const view = document.createElement('canvas')
document.body.append(view)

renderText({
  view,
  fonts,
  style: { width: 300, fontSize: 22, textDecoration: 'underline' },
  content: [
    {
      letterSpacing: 3,
      fragments: [
        { content: 'He', color: 'red', fontSize: 12 },
        { content: 'llo', color: 'black' },
      ],
    },
    { content: ', ', color: 'grey' },
    { content: 'World!', color: 'black' },
  ],
})

Layout backend

modern-text measures with a single built-in Measurer — pure-JS and DOM-free — which resolves fonts from the fonts you pass or from modern-font's global registry. It's the default; to plug in your own engine, pass any object implementing TextMeasurer:

new Text({ fonts }) // uses the built-in Measurer (default)
new Text({ measurer: myCustomMeasurer }) // any object implementing TextMeasurer

Node / SSR / Workers

The built-in Measurer needs no document, so the whole measure → render pipeline runs outside the browser. Register fonts from a buffer with modern-font:

import { readFileSync } from 'node:fs'
import { Fonts, parseFont } from 'modern-font'
import { Text } from 'modern-text'

const buffer = readFileSync('./fonts/NotoSansSC.woff').buffer
const font = parseFont(buffer)
const sfnt = font.createSFNT() // .woff → SFNT
const fonts = new Fonts()
const entry = { src: '', familySet: new Set(['Noto']), buffer, getFont: () => font, getSFNT: () => sfnt } as any
fonts.set('Noto', entry)
fonts.setFallbackFont(entry)

const text = new Text({ fonts, content: '你好世界', style: { fontFamily: 'Noto', fontSize: 32 } })
const result = text.measure() // → boxes for every paragraph / fragment / character

Content model

Content is a hierarchy: Text → Paragraph → Fragment → Character. Each level inherits and merges style downward. content accepts several shapes that are normalized by modern-idoc:

// a plain string (single paragraph)
content: 'Hello World'

// an array of paragraphs, each a string or { content, ...paragraphStyle }
content: [
  { content: 'Title', fontSize: 40, textAlign: 'center' },
  { content: 'Body text', color: '#333' },
]

// per-fragment styling inside a paragraph
content: [
  {
    textAlign: 'center',
    fragments: [
      { content: 'red ', color: 'red' },
      { content: 'bold', fontWeight: 'bold' },
    ],
  },
]

A newline (\n) splits into a new paragraph.

Styling

Style can be set at the text (root), paragraph, or fragment level.

style: {
  // box
  width: 400, height: 200, padding: 16,
  // font
  fontSize: 24, fontFamily: 'Arial', fontWeight: 700, fontStyle: 'italic',
  // text
  color: '#222', lineHeight: 1.4, letterSpacing: 1, textIndent: 24,
  textAlign: 'center',          // start | left | center | end | right
  verticalAlign: 'middle',      // top | middle | bottom
  writingMode: 'vertical-rl',   // horizontal-tb | vertical-rl
  textDecoration: 'underline',  // underline | line-through | overline | none
  textTransform: 'uppercase',   // uppercase | lowercase
  textStrokeWidth: 2, textStrokeColor: '#000', // outline stroke
}

Gradient fills

content: [{
  fragments: [{
    content: 'Gradient',
    fill: {
      linearGradient: {
        angle: 180,
        stops: [
          { color: '#c7f1ff', offset: 0 },
          { color: '#ffffff', offset: 1 },
        ],
      },
    },
  }],
}]

Highlights & list markers

content: [
  // image drawn behind the fragment
  { fragments: [{ content: 'highlighted', highlightImage: '/brush.svg' }] },
  // list bullet
  { content: 'a bullet item', listStyleType: 'disc' },
  { content: 'a custom bullet', listStyleImage: '/dot.svg' },
]

Effects

effects is an ordered stack of transform/color layers drawn behind the main glyphs — useful for shadows, 3D offsets and outlines. translateX/Y are fractions of the font size; skewX/Y are degrees.

renderText({
  view,
  fonts,
  content: 'Effect',
  style: { fontSize: 80, color: '#FEE90C' },
  effects: [
    { translateX: 0.05, translateY: 0.05, skewY: -5, color: '#000' }, // shadow
    { skewY: -5, color: '#FEE90C' }, // face
  ],
})

Text deformation

Deformation presets are an opt-in subpath. Register them once, then set deformation.type:

import { registerDeformations } from 'modern-text/deformations'
import { renderText } from 'modern-text'

registerDeformations()

renderText({
  view,
  fonts,
  content: 'Deformation',
  style: { fontSize: 64 },
  deformation: { type: 'arch-curve', intensities: [50] },
})

intensities is the strength of the effect, 0100 per axis. Most presets read a single value (intensities[0]); a few are two-axis and also read intensities[1] — e.g. skew and trapezoid slant/taper independently on the x and y axes. A missing axis is treated as 0.

Deformation is scale-invariant: the geometry is derived from the text's own font size, so the same intensities produce the same shape at any fontSize — you don't pass a reference size. (The legacy deformation.maxFontSize field is no longer needed and is only used as a fallback when no glyphs are measurable.)

bend · bend-vertical · arch-curve · concave-curve · upper-arch-curve · lower-arch-curve · bulb-curve · skew · flag-curve · trapezoid · lower-trapezoid · top-trapezoid · horizontal-trapezoid · bevel · upper-roof · lower-roof · angled-projection · folded-corner · lateral-stretching · vertical-stretching · patchwork-by-word · step-by-word · arch2-by-word · wave-by-word · step-far-and-near-by-word · arch-far-and-near-by-word · horizontal-rotate-by-word · arbitrary-offset-rotate-by-word · horizontal-curved-rotate-by-word · ellipse-by-word · triangle-by-word · pentagon-by-word · rectangular-by-word · heart-by-word

Register your own with defineDeformation(name, preset).

Text API

For finer control, drive a Text instance directly:

import { Text } from 'modern-text'

const text = new Text({ fonts, content: 'Hello', style: { fontSize: 24 } })

text.on('update', () => text.render({ view })) // re-render on any change
await text.load() // load async resources (fonts, plugin assets)
text.update() // measure + commit + emit 'update'
text.render({ view, pixelRatio: 2 })

text.boundingBox // overall box after measuring
text.characters // flat list of measured Character (inlineBox / lineBox / path)

text.dispose() // release the cached measurer / renderer
  • measure() returns a non-destructive snapshot of all boxes.
  • update() measures and commits the result onto the instance.
  • render({ view }) updates if needed, then draws.
  • Events: update, measure, render.

One-shot helpers

import { measureText, renderText } from 'modern-text'

const result = measureText(options) // sync
const result = await measureText(options, true) // load fonts first

renderText({ view, ...options }) // sync
await renderText({ view, ...options }, true) // load fonts first

<text-editor> web component

import { TextEditor } from 'modern-text/web-components'

TextEditor.register()
<text-editor></text-editor>
const editor = document.querySelector('text-editor')
editor.moveToDom(canvas) // overlay the editor on a rendered canvas
editor.set(text) // bind a Text instance — provides cursor, selection, typing

License

MIT