# opentype.js: parsing and building OpenType fonts in the browser and Node.js

> opentype.js reads OTF, TTF and WOFF files into a JavaScript Font object and writes fonts back out. It is a parser and a builder, not a renderer, and the WOFF2 gap is the first thing to check before adopting it.

**opentypejs/opentype.js** — Read and write OpenType fonts using JavaScript.

- Repository: https://github.com/opentypejs/opentype.js
- Website: https://opentype.js.org/
- Stars: 5,025 · Forks: 555
- Language: JavaScript
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/opentypejs-opentype-js

## What opentype.js does that a CSS @font-face rule cannot

A browser will happily render a font you never inspect. opentype.js exists for the cases where you need the letterforms themselves as data. The README describes the scope plainly: it "gives you access to the letterforms of text from the browser or Node.js." That means you can pull a glyph's outline out as a bézier path and draw it yourself, measure an advance width, read kerning pairs from either GPOS or the older kern table, or assemble a font file from paths you generated.

The audience is narrow but real. People building a glyph inspector or font debugging tool need per-glyph access. People generating PDFs or SVG server-side need outlines rather than a rasterised bitmap. People writing a font editor, or a build step that subsets or rewrites a font, need both directions: parse in, write out. The README's own demo at opentype.js.org is a live example of the first category.

It is not a text layout engine. It gives you glyphs and paths; line breaking, bidi reordering, shaping across scripts and justification are not what this library is. Arabic rendering is listed as a feature and traced to issue #364 and PRs #359 and #361, so some script handling exists, but the feature list is a list of font-table capabilities, not a claim about full text shaping.

## How parsing works: ArrayBuffer in, Font object out

The data flow has two stages and the README is explicit that they are separate. First the font file becomes an ArrayBuffer. The README shows three ways to get one: a fetch from a URL, fs.promises.readFile in Node, or files[0].arrayBuffer() from an <input type=file>. Second, that buffer goes through opentype.parse() and comes back as a Font instance.

The Font object is where the useful structure lives. According to the README it holds glyphs as an indexed list of Glyph objects, plus unitsPerEm, ascender and descender. unitsPerEm is the coordinate grid the font was drawn on, and the README names 2048 and 4096 as common values, which matters because ascender and descender are expressed in font units rather than pixels. Any conversion to screen or page coordinates is your arithmetic, not the library's.

Under the hood the parser handles both outline flavours: TrueType glyf and PostScript cff, with WOFF and OTF and TTF all accepted as containers. Composite glyphs (the accented letters that reference other glyphs) are supported, and TrueType hinting is listed too. There is a low memory mode offered as an option, traced in the README to issue #329, which is the kind of switch that exists because parsing large fonts otherwise holds a lot in memory at once.

## Installing opentype.js and parsing your first font

The npm package is the normal route, and the README gives the install command directly. Run it in your project root:

```bash
npm install opentype.js
```

The README shows three import styles depending on your module system: require, a default import, and a named import of load. Pick the one that matches your build. For a CommonJS script the require form is the shortest path.

```js
const opentype = require('opentype.js');
```

Now load a font from disk and parse it. The README's pattern is to read the file into a buffer first, then hand that buffer to parse. In an async context that is two lines:

```js
const buffer = require('fs').promises.readFile('./my.woff');
const font = opentype.parse(await buffer);
console.log(font);
```

If you are not in an async context, the README shows the promise form instead, resolving the buffer and calling opentype.parse(data) inside the then callback. Either way, what you should see on the console is a Font object rather than a raw buffer: the parse step is what turns bytes into glyphs, metrics and tables.

If you would rather not install anything, the README lists CDN sources at opentype.js.org/dist/opentype.js, cdn.jsdelivr.net/npm/opentype.js and unpkg.com/opentype.js. The global script tag exposes an opentype global; the module form needs the full path to dist/opentype.module.js and imports parse by name.

## Writing fonts out, and the WOFF2 decision you have to make yourself

The library runs in both directions. A Font object, whether it came from parse() or from being constructed by hand, can be serialised with toArrayBuffer(). In Node the README writes that buffer to disk with fs.writeFileSync and Buffer.from; in the browser it creates a Blob with the type font/opentype, builds an object URL, and clicks an anchor element to trigger the download.

Building a font from scratch is a documented path too. You create a Path, issue moveTo and lineTo calls, wrap the path in a Glyph with a name, a unicode value and an advanceWidth, and pass an array of glyphs to the Font constructor along with familyName, styleName, unitsPerEm, ascender and descender. One detail worth noting: the README says the .notdef glyph is required, so a hand-built font without it is not a valid starting point.

WOFF2 is the sharp edge. The README explains that WOFF2's Brotli compression performs 29% better than WOFF, but that supporting it would make the library roughly ten times heavier, going from about 120KB to about 1400KB. The project's answer is to keep the library small and tell you to decompress beforehand, pointing at fontello/wawoff2. The README's example loads wawoff2's decompress_binding.js from unpkg, waits for the runtime to initialise, and calls Module.decompress before passing the result to opentype.parse. That is a real integration cost: an extra dependency, an extra network fetch, and a runtime-ready wait you have to get right.

## Where opentype.js is the wrong tool

The WOFF2 story is the clearest boundary. If your pipeline receives WOFF2 files from a CDN or a vendor and you have no control over the format, you are signing up for wawoff2 as a companion dependency and the initialisation dance the README demonstrates. If you cannot add that, look elsewhere or convert the fonts upstream.

Memory is the second constraint. The low memory mode is an option rather than the default, which tells you the default path holds more in memory. For a single font in a browser tab that is irrelevant. For a server process parsing many fonts concurrently, it is a parameter you should be setting deliberately rather than inheriting.

The third boundary is scope. The feature list covers outlines, kerning, ligatures, hinting, Arabic text, colour glyphs and emoji. It does not claim to be a renderer or a layout engine. If what you actually want is text on a canvas with correct line breaking, you will end up writing the layout layer yourself on top of the paths this library hands you, and that layer is not small.

Finally, the project's own release history is worth reading before you pin a version. The 1.3.5 entry is labelled "2.0.0 prerelease (Accidentally released as 1.3.5)", so that version number on npm does not mean what the number suggests. The clean line is 2.0.0, released on 2026-05-06, after 1.3.4 in November 2023. The last push to the repository was on 2026-08-08.

## opentype.js compared with fontkit and opentype.js's own limits

The obvious alternative in JavaScript is fontkit, and the difference is one of design centre rather than feature checklists. fontkit is built around layout: it exposes a layout engine that takes a string and a set of features and returns positioned glyph runs, handling shaping, script-specific substitution and vertical metrics as part of the API. opentype.js exposes the font's contents and leaves positioning to you. If your problem is "render this paragraph correctly in a complex script", the two libraries are not interchangeable and fontkit's approach fits better. If your problem is "give me the outline for glyph 65" or "write this glyph set back out as an OTF", opentype.js is the more direct fit and you avoid carrying a layout engine you will not call.

A second alternative is to skip JavaScript parsing entirely and shell out to a native tool such as fontTools, then ship the result. That buys you the mature Python ecosystem and its subsetting and conversion utilities, at the cost of a build step and a language boundary. For a browser-side glyph inspector, that trade is usually not worth it; for a server-side font build pipeline, it often is.

Within opentype.js itself, the honest limitation is that the README is a usage document, not a table-by-table specification. It does not enumerate which OpenType tables the parser reads or which it ignores. If your fonts carry unusual layout or colour tables, the README will not tell you whether they survive a round trip, and you will find out by parsing one and inspecting the result.

## Licence, release process and what upgrades cost

The package is MIT licensed, stated in package.json and in the repository's LICENSE file. MIT is permissive, so redistributing the library inside a larger product is normally straightforward, but the terms are in that file and the file is what governs. This is not legal advice; read the LICENSE before you ship.

Upgrade cost is tied to the release process, which the README documents in unusual detail. Releases go out through a manual GitHub Actions workflow named Publish package to npm. The maintainer confirms a target version, ensures master is green, runs the workflow with dryRun: true and npmDistTag: latest, inspects the dry-run output for the package version and packed files, then re-runs with dryRun: false. The workflow then runs preflight checks, publishes to npm, and creates the matching git tag and GitHub release.

The practical consequence is that the published artefact is the one inspected during the dry run, which reduces the chance of a mismatched tag. It also means releases are deliberate rather than continuous. With 1.3.4 in November 2023 and 2.0.0 in May 2026, the gap between them is long enough that moving from 1.x to 2.x is a project, not a patch bump. Read the 2.0.0 release notes before you plan that move. For local work, the README's contributor flow is npm install, then npm run build for a simple build or npm run start for a dev server on the /docs folder, with npm run test to check nothing broke.

## Conclusion

Adopt opentype.js when you need glyph outlines, kerning or ligature data in JavaScript and you control the font files you feed it. Do not adopt it as a text layout engine or as a WOFF2 decoder, because the README states WOFF2 support would multiply the library size by more than ten and the project instead points at wawoff2 for decompression. Verify two things before committing: that your target fonts do not use colour or layout tables the parser does not cover, and that the MIT licence terms in the repository's LICENSE file fit how you redistribute the library.

## FAQ

### How do I use opentype.js to load a font?

Load the font file into an ArrayBuffer, either from a URL with fetch, from the filesystem with fs.promises.readFile, or from a file input in the browser, then pass that buffer to opentype.parse(). The call returns a Font instance holding the glyphs and metrics.

### What is opentype.js?

It is a JavaScript library that reads and writes OpenType fonts, described in package.json as an OpenType font parser. It gives access to letterforms from the browser or Node.js and supports WOFF, OTF and TTF files with either glyf or cff outlines.

### Is OpenType better than TrueType?

opentype.js treats them as two outline flavours inside the same parser rather than as competing formats: the README lists support for OTF and TTF, with TrueType glyf and PostScript cff outlines both handled. Which one you pick is a question about your font files, not about this library.

## Sources

- [License: MIT](https://github.com/opentypejs/opentype.js/blob/master/LICENSE)
- [opentypejs/opentype.js on GitHub](https://github.com/opentypejs/opentype.js)
- [Project website](https://opentype.js.org/)
- [README](https://github.com/opentypejs/opentype.js/blob/master/README.md)
- [Releases](https://github.com/opentypejs/opentype.js/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/opentypejs-opentype-js
