A type designer's desk showing letterforms becoming vector node points, with a large red Maine Coon cat resting on font catalogs nearby.

opentype.js: Turning Letters Into Vectors You Can Actually Touch

The Orange Cat
The Orange Cat

Every web developer has rendered text a thousand times, but almost none of us have ever held a letter. The browser draws fonts through CSS and then locks the geometry away — you cannot ask it where the curve of an "S" bends, how wide a kerned "AV" pair really is, or what the outline of your logo looks like as editable vectors. opentype.js opens that locked door. It is a parser and writer for OpenType and TrueType fonts that reads the raw binary tables of a font file and exposes them as friendly JavaScript objects: a Font made of Glyphs, each carrying a vector Path you can measure, transform, draw, or export.

The best part is what it does not drag along. opentype.js has zero runtime dependencies, weighs around 120 KB minified, and behaves identically in the browser and in Node.js with no native bindings. Despite being a "lesser-known" name, it quietly powers an enormous slice of the JavaScript typography world — racking up close to a million weekly downloads as the engine behind text-to-SVG converters, in-browser font editors, generative type art, and server-side OG image renderers. If you have ever wanted to turn a string into crisp <path> data without a backend, this is the tool.

What You Get In The Box

opentype.js is deliberately low-level and font-focused, which is exactly why it is so flexible. Its core capabilities include:

  • Reading fonts from OTF, TTF, and WOFF files into a queryable object model.
  • Glyph geometry access — every glyph exposes a Path built from move, line, cubic, and quadratic curve commands.
  • SVG and canvas output — convert any string into SVG path data or draw it straight onto a 2D canvas context.
  • Precise metrics — advance widths, side bearings, bounding boxes, and kerning pair values.
  • Writing fonts — build glyphs and a Font from scratch and serialize to a downloadable .otf or .ttf.
  • Modern font features — kerning, GSUB ligatures, variable fonts, and color fonts.

It handles Latin, Arabic, emoji, color glyphs, and variable axes. What it intentionally is not is a full text-shaping engine for complex scripts at the level of HarfBuzz — but for reading and writing letterform geometry, it is wonderfully direct.

Getting It Installed

Add it with your package manager of choice:

npm install opentype.js
yarn add opentype.js

If you would rather skip the bundler entirely, you can pull the ES module straight from a CDN:

<script type="module">
  import { parse } from "https://unpkg.com/opentype.js/dist/opentype.module.js";
</script>

Loading A Font Into Memory

In version 2, the idiomatic pattern is a clean two-step: obtain an ArrayBuffer, then hand it to opentype.parse(). This sidesteps all the old browser-versus-Node path ambiguity, because you control exactly how the bytes arrive.

import opentype, { Font } from 'opentype.js';

// In the browser, fetch the font bytes from a URL
async function loadFromUrl(url: string): Promise<Font> {
  const buffer = await fetch(url).then((r) => r.arrayBuffer());
  return opentype.parse(buffer);
}

// In Node.js, read the file off disk
import { readFile } from 'node:fs/promises';

async function loadFromDisk(path: string): Promise<Font> {
  const buffer = await readFile(path);
  return opentype.parse(buffer);
}

// From a file input the user picked
async function loadFromInput(input: HTMLInputElement): Promise<Font> {
  const buffer = await input.files![0].arrayBuffer();
  return opentype.parse(buffer);
}

The same parse call works for OTF, TTF, and WOFF. The older callback-style opentype.load(url, callback) and Node's opentype.loadSync(path) still exist and were not removed, but the fetch-then-parse flow is the recommended path going forward — it is just an ArrayBuffer in, a Font out.

From String To Vector Path

Once you hold a Font, the fun begins. The single most useful method is getPath, which lays out a whole string and returns one combined Path positioned at a baseline you choose.

// font.getPath(text, x, y, fontSize, options)
const path = font.getPath('Hello', 0, 150, 72);

// Turn it into SVG path data — an "M..." string ready for a <path d="">
const d = path.toPathData({ decimalPlaces: 2 });

// Or get a full <path/> element string
const svgElement = path.toSVG({ decimalPlaces: 2 });

// Need each glyph separately? Get an array of paths
const paths = font.getPaths('Hello', 0, 150, 72);

Note that the y argument is the baseline, not the top of the text — font coordinate space puts Y going up, while SVG and canvas put Y going down. The toPathData and toSVG methods handle this for you with a flipY: true default, so your exported paths land right-side-up.

Prefer pixels over markup? Draw directly to a canvas, and use the built-in debug helpers when you want to see what the font is doing:

const ctx = canvas.getContext('2d')!;
font.draw(ctx, 'Hello', 0, 150, 72);

// Visualize the raw on/off-curve control points
font.drawPoints(ctx, 'Hg', 0, 150, 72);

// Visualize side bearings, bounding box, and advance width
font.drawMetrics(ctx, 'Hg', 0, 150, 72);

Measuring Letters Down To The Pixel

Layout work lives and dies on metrics, and opentype.js gives you the real numbers — including kerning — rather than guesses.

// Total advance width of a string at a given size, kerning included
const width = font.getAdvanceWidth('Hello', 72);

// A single glyph, or null if the character is missing
const glyph = font.charToGlyph('A');

// Watch out: ligatures mean glyph count can differ from character count
const glyphs = font.stringToGlyphs('Hi');

if (glyph) {
  const bbox = glyph.getBoundingBox(); // { x1, y1, x2, y2 }
  const raw = glyph.advanceWidth;       // unscaled font units
  const lsb = glyph.leftSideBearing;
}

// The kern value between two glyphs
const kern = font.getKerningValue(glyphs[0], glyphs[1]);

Two gotchas are worth tattooing on the back of your hand here. First, stringToGlyphs can return fewer glyphs than you have characters, because ligatures like "fi" collapse into a single glyph — never assume one character equals one glyph when mapping results back to text. Second, charToGlyph can hand you back null or the .notdef placeholder for characters the font does not contain, so always guard the gap.

Tuning The Render With Features

The fifth argument to getPath, getPaths, and draw is an options object that controls how text is shaped and styled. This is where you toggle OpenType features like ligatures, switch scripts, or pull colors out of a color font.

const path = font.getPath('Affluent', 0, 150, 72, {
  script: 'latn',
  language: 'dflt',
  kerning: true,
  features: { liga: true, rlig: true }, // required and contextual ligatures
  hinting: false,
  fill: '#222222',
  colorFormat: 'hexa', // for color fonts: 'rgb' | 'rgba' | 'hex' | 'bgra' | 'raw'
});

Kerning is on by default and pulls from the font's GPOS or legacy kern tables. The features map lets you flip individual OpenType features on and off, which is how you control whether "Affluent" renders with its decorative "ffl" ligature or stays as discrete letters.

Building A Font From Nothing

Reading fonts is only half the story. opentype.js can assemble a font from scratch and serialize it to a real, installable file. You construct Path outlines, wrap them in Glyphs, and gather those into a Font.

import opentype from 'opentype.js';

const notdef = new opentype.Glyph({
  name: '.notdef',
  advanceWidth: 650,
  path: new opentype.Path(),
});

const aPath = new opentype.Path();
aPath.moveTo(100, 0);
aPath.lineTo(100, 700);
aPath.lineTo(550, 700);
aPath.lineTo(550, 0);
aPath.close();

const aGlyph = new opentype.Glyph({
  name: 'A',
  unicode: 65,
  advanceWidth: 650,
  path: aPath,
});

const font = new opentype.Font({
  familyName: 'WhiskerSans',
  styleName: 'Medium',
  unitsPerEm: 1000,
  ascender: 800,
  descender: -200,
  glyphs: [notdef, aGlyph],
});

// In the browser, trigger a download
font.download();

// Or grab the raw bytes yourself
const bytes = font.toArrayBuffer();
// Node: writeFileSync('whiskersans.otf', Buffer.from(bytes));

If your letterforms already exist as artwork, you do not have to plot every point by hand. Path.fromSVG('M0 0 ...') parses SVG path data straight back into a glyph outline, which makes importing icons or hand-drawn shapes into a font almost trivial. This is the round-trip that turns a folder of SVG icons into a custom icon font at runtime.

Going Variable And Colorful

Version 2.0.0 was a major release that brought modern font technology into reach. Variable fonts are now first-class: opentype.js parses the fvar, STAT, avar, gvar, and cvar tables and exposes a Font.variation manager so you can select a named instance or dial in custom axis coordinates and render the interpolated outlines.

// Render at a specific point in the design space
font.variation.set({ wght: 650, wdth: 110 });
const path = font.getPath('Variable', 0, 150, 72);

// Or jump to a named instance the designer baked in
font.variation.set('SemiBold');

The same release added COLRv0 and CPALv0 color font support through the Font.palettes and Font.layers managers, CFF2 parsing for modern PostScript outlines, extended cmap formats 13 and 14 with Unicode Variation Sequences, and OpenType-SVG glyph tables. For most existing code the upgrade is non-breaking, since the classic loading API still works — but if you touch internal table structures or the color and variable APIs, give the changelog a read.

One deliberate omission deserves a callout: WOFF2 is not decompressed internally. The maintainers chose not to bundle the Brotli-based WOFF2 decoder because it would balloon the library from roughly 120 KB to around 1,400 KB. WOFF2 files are now gracefully detected, but you must pre-decompress them to an ArrayBuffer with an external helper such as wawoff2 before calling parse. Plain WOFF, OTF, and TTF all work directly with no extra steps.

Dropping It Into React

Although opentype.js is framework-agnostic, it slots into React naturally. Load the font once, keep the parsed Font in a ref, and render its path data into an SVG or a canvas.

import { useEffect, useRef, useState } from 'react';
import opentype, { Font } from 'opentype.js';

function TextToSvg({ text }: { text: string }) {
  const fontRef = useRef<Font | null>(null);
  const [pathData, setPathData] = useState('');

  useEffect(() => {
    let cancelled = false;
    fetch('/fonts/Roboto.woff')
      .then((r) => r.arrayBuffer())
      .then((buffer) => {
        if (cancelled) return;
        fontRef.current = opentype.parse(buffer);
        setPathData(fontRef.current.getPath(text, 0, 72, 72).toPathData());
      });
    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    if (fontRef.current) {
      setPathData(fontRef.current.getPath(text, 0, 72, 72).toPathData());
    }
  }, [text]);

  return (
    <svg viewBox="0 0 600 100" width="600" height="100">
      <path d={pathData} fill="currentColor" />
    </svg>
  );
}

Because it has zero runtime dependencies and runs entirely in the browser, it drops cleanly into a Vite or Next.js client component. The only caveat is to guard the draw and canvas APIs against server-side rendering, since they expect a real <canvas> to exist.

Where It Fits In The Ecosystem

It helps to know when opentype.js is the right pick. fontkit, the engine behind react-pdf, supports more formats and offers advanced layout and subsetting, but it is heavier and more Node-oriented — better for PDF and print pipelines. harfbuzzjs is the gold standard for shaping complex scripts, but it shapes text rather than handing you a friendly Font/Glyph/Path model, so it is often paired with opentype.js rather than replacing it. Tiny parsers like Typr.js are fast but have a smaller API and no real font-writing story.

opentype.js wins on the combination almost nobody else offers: zero dependencies, equal footing in the browser and Node, clean SVG path output, and the ability to write fonts — all in about 120 KB. It is the quiet foundation under text-to-SVG tools, glyph inspectors, kerning editors, generative type art, runtime font generators, and server-rendered text images. If your project needs to reach past CSS and actually hold the shapes of letters, this is the library that puts them in your hands.