Creating NPM Font Packages
Learn how to create, structure, and publish NPM packages for fonts. Enable easy font distribution, versioning, and integration with modern JavaScript frameworks like React and Next.js.
TL;DR - Key Takeaways
- • Use WOFF2 format for smallest package size (70-90% smaller than TTF)
- • Export separate CSS files for each font weight to enable tree-shaking
- • Follow semantic versioning—major for breaking changes, minor for new weights
- • Consider Fontsource pattern for compatibility with existing ecosystem
In this article
Distributing fonts via NPM packages offers significant advantages over traditional approaches. Developers can install fonts as dependencies, lock specific versions, and benefit from bundler integration for optimal loading. This is particularly powerful for design systems and component libraries where typography consistency is critical.
This guide covers creating custom font packages from scratch, following established patterns like Fontsource, and publishing to NPM or private registries. Whether you're packaging custom brand fonts or creating reusable font utilities, these techniques ensure professional distribution.
The key benefits include version-controlled font updates, simplified installation via package managers, integration with bundlers like Webpack and Vite, and the ability to include font metadata and CSS alongside font files.
Package Structure
A well-organized font package separates font files, CSS, and documentation for flexibility.
Recommended Structure
@company/font-inter/ ├── package.json ├── README.md ├── LICENSE ├── index.css # All weights combined ├── files/ │ ├── inter-latin-400-normal.woff2 │ ├── inter-latin-400-italic.woff2 │ ├── inter-latin-500-normal.woff2 │ ├── inter-latin-600-normal.woff2 │ └── inter-latin-700-normal.woff2 ├── 400.css # Only 400 weight ├── 500.css # Only 500 weight ├── 600.css # Only 600 weight ├── 700.css # Only 700 weight └── variable.css # Variable font (if available)
Individual Weight CSS
Separate CSS files for each weight enable tree-shaking—only import what you use.
// Only import needed weights import '@company/font-inter/400.css'; import '@company/font-inter/700.css';
Combined Index CSS
A single import for all weights when bundle size isn't a concern.
// Import all weights import '@company/font-inter'; // or import '@company/font-inter/index.css';
package.json Configuration
Proper package.json configuration ensures compatibility with bundlers and correct file resolution.
{
"name": "@company/font-inter",
"version": "1.0.0",
"description": "Inter font family for web",
"main": "index.css",
"style": "index.css",
"files": [
"files/",
"*.css"
],
"exports": {
".": "./index.css",
"./400.css": "./400.css",
"./500.css": "./500.css",
"./600.css": "./600.css",
"./700.css": "./700.css",
"./variable.css": "./variable.css"
},
"keywords": ["font", "inter", "webfont", "woff2"],
"license": "OFL-1.1",
"repository": {
"type": "git",
"url": "https://github.com/company/fonts"
},
"sideEffects": ["*.css"]
}Important: sideEffects
The sideEffects field tells bundlers that CSS files should not be tree-shaken even if no exports are used. Without this, bundlers might incorrectly remove your CSS.
CSS @font-face Structure
Each CSS file should include optimized @font-face declarations with proper font-display and format specification.
400.css
/* Inter Latin 400 Normal */
@font-face {
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url('./files/inter-latin-400-normal.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
U+FFFD;
}Unicode Range
Include unicode-range for subsetting. Browsers only download fonts when characters in that range are needed.
font-display: swap
Ensures text is immediately visible with a fallback font, then swaps to the custom font when loaded.
Variable Font Support
Variable fonts allow a single file to contain all weights, reducing HTTP requests and total download size.
Variable Font CSS
/* variable.css */
@font-face {
font-family: 'Inter Variable';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url('./files/inter-latin-variable.woff2') format('woff2-variations');
}
/* Optional: CSS custom property for weight */
:root {
--font-inter: 'Inter Variable', 'Inter', system-ui, sans-serif;
}Variable Font Naming
Use a distinct font-family name for variable fonts (e.g., "Inter Variable") so developers can explicitly choose between static and variable versions based on browser support needs.
Publishing to NPM
Follow these steps to publish your font package to NPM or a private registry.
1. Verify Package Contents
# Preview what will be published npm pack --dry-run # Check package size npm pack && ls -la *.tgz
2. Version and Tag
# Bump version npm version patch # 1.0.0 -> 1.0.1 npm version minor # 1.0.0 -> 1.1.0 (new weights) npm version major # 1.0.0 -> 2.0.0 (breaking changes)
3. Publish
# Public package npm publish --access public # Scoped private package npm publish # Private registry npm publish --registry https://registry.company.com
Usage in Projects
Once published, developers can install and use your font package easily.
Next.js / React
// app/layout.tsx or _app.tsx
import '@company/font-inter/400.css';
import '@company/font-inter/700.css';
// tailwind.config.js
module.exports = {
theme: {
fontFamily: {
sans: ['Inter', 'system-ui'],
},
},
}Vite / Plain JS
// main.js or main.ts
import '@company/font-inter';
// styles.css
body {
font-family: 'Inter', sans-serif;
}Convert Fonts for NPM Distribution
Convert your fonts to WOFF2 format optimized for NPM package distribution.
Start Converting FontsWritten & Verified by
Sarah Mitchell
Product Designer, Font Specialist
NPM Font Packages FAQs
Common questions about creating and distributing font packages
