Download every web font from any site into a project-ready folder — with CSS, manifest, and framework configs ready to drop in.
npx fontfetch https://shinobidata.com→ Fetching page: https://shinobidata.com
3 external stylesheet(s), 0 inline <style> block(s)
→ Found 12 @font-face declaration(s), 18 unique file(s)
✓ Inter-Regular.woff2 (32,180 bytes)
✓ Inter-SemiBold.woff2 (28,044 bytes)
...
Done. 18/18 files saved to ./downloaded-fonts/shinobidata.com
That's it. Real font files, a ready-to-paste fonts.css with local URLs, a JSON manifest, and a README — all in one folder you can drag straight into public/fonts/.
You're mocking up a design. You see a font you like on a marketing site. You want to test it locally for a few hours of iteration — not ship it to production, just see how your design feels with that typography.
The existing options aren't great:
google-webfonts-helper— beautiful, but Google Fonts onlywebfont-dl— works, but you have to find the CSS URL yourself- Chrome extensions — point-and-click, no automation, no project integration
fontfetch takes a URL. Returns a folder. That's the whole product.
downloaded-fonts/
└── shinobidata.com/
├── files/ ← raw woff2 / woff / ttf / otf
│ ├── Inter-Regular.woff2
│ ├── Inter-SemiBold.woff2
│ └── ...
├── fonts.css ← @font-face block with local URLs
├── fonts.json ← manifest: family / weight / style / files
└── README.md ← human-readable summary, grouped by family
Drop the folder into public/fonts/ (or wherever), link fonts.css, done.
Run on demand:
npx fontfetch <url>Install globally:
npm install -g fontfetch
fontfetch <url>Or pick the distribution channel that fits your workflow (v1.4):
# Homebrew tap (once published — see extensions/homebrew/)
brew install niyamvora/fontfetch/fontfetch
# GitHub Action (PR comments on font drift, CI release-gate)
# uses: niyamvora/fontfetch-action@v1
# See extensions/github-action/README.md
# Raycast extension (Cmd-Space → Extract Fonts from URL)
# See extensions/raycast/README.md
# Programmatic access to the pairings registry
npm install @fontfetch/registryRequires Node 18+.
fontfetch <url> [outDir] [--headless] [--pages <N>] [--fallback] [--emit ...] [--formats ...] [--force]
fontfetch inspect <font-file>
fontfetch subset <url> [outDir] [--whitelist <spec>] [--split-ranges[=<buckets>]]
fontfetch diff <urlA> <urlB> [outDir] [--json] # v1.4
fontfetch audit <url> [--max-kb N] [--per-family-kb F:N,...] [--no-commercial] [--json] # v1.4
fontfetch budget <url> --max-kb N [outDir] [--json] # v1.4
fontfetch morph <font-file> [--round N] [--width N] [--slant N] [--weight N] [--rename <name>] [--out <dir>] [--format ttf|woff2] [--json] # v1.5| Arg / Flag | Default | Notes |
|---|---|---|
<url> |
— | Page to download fonts from (use the page where the font is actually rendered) |
[outDir] |
./downloaded-fonts |
Per-site subfolder is created inside this |
--headless |
off | Launch Playwright/Chromium to also catch JS-loaded fonts |
--pages <N> |
1 |
Crawl up to N pages (entry + N-1 same-origin internal links) and merge fonts across all of them (v1.2.1). Max 50 |
--formats <list> |
— | Comma-separated allowlist of font formats to keep: woff2, woff, ttf, otf, eot. Faces with no matching source are dropped (v1.3). Default: keep every format the upstream CSS provides |
--fallback |
off | Emit a CLS-killing <Family> Fallback @font-face per family, with size-adjust / ascent-override / descent-override / line-gap-override matched via capsize metrics (v1.2). v1.3.1: monospace detection now reads the binary's post.isFixedPitch flag, not just the family name. v1.4: emits one block per (family, weight, style) tuple |
--gdpr-report |
off | Emit GDPR.md + gdpr.json listing every third-party font request with self-host remediation (v1.4) |
--emit <list> |
— | Framework configs: next, tailwind, vite, tokens (v1.4), css (default) |
--force |
off | Bypass the fail-fast check that blocks all-commercial sites |
--whitelist <spec> (subset) |
— | Extra codepoints to always include, on top of the DOM walk. CSS unicode-range syntax: U+00A0,U+20AC,U+0020-007F (v1.3) |
--split-ranges[=<buckets>] (subset) |
off | Emit one woff2 per Google Fonts language bucket (latin, latin-ext, cyrillic, cyrillic-ext, greek, greek-ext, vietnamese) and a chained fonts.subset.css (v1.3) |
--round / --width / --slant / --weight <N> (morph) |
— | Parametric morph sliders: corner radius 0–100%, width 80–120%, slant 0–15°, stroke −15…+15% (v1.5). --weight on static fonts is experimental |
--rename <name> (morph) |
"<original> Prototype" |
Output family name (v1.5) |
--format <ttf|woff2> (morph) |
woff2 if input was woff2, else ttf | Output binary format. Accepts TTF / OTF / WOFF / WOFF2 input either way (v1.5) |
Examples:
fontfetch https://shinobidata.com
fontfetch https://linear.app ./public/fonts
fontfetch https://vercel.com /tmp/scratch
fontfetch https://some-spa.com --headless
fontfetch https://acme.com --pages=5
fontfetch https://shinobidata.com --formats=woff2
fontfetch https://stripe.com --headless --fallback --emit next
fontfetch inspect ./downloaded-fonts/example.com/files/google/Inter-Variable.woff2
fontfetch subset https://stripe.com
fontfetch subset https://stripe.com --whitelist=U+00A0,U+20AC
fontfetch subset https://stripe.com --split-ranges
fontfetch morph ./Inter.ttf --round=20 --width=108
fontfetch morph ./Geist.otf --slant=8 --rename "Geist Sketch"The first feature that gives fontfetch a moat its CLI competitors can't reach. Extract any font, then sketch on it. Built for pre-commission ideation — a typography sketchbook that comes before commissioning a real typeface, not a replacement for one.
fontfetch morph <file>— parametric morphing with four sliders. Round corners, widen/condense, slant (faux-oblique), and thicken/thin the stroke, then export a real binary. Width and slant are lossless matrix transforms; rounding fillets straight-line corners;--weighton static fonts is an experimental, clamped outline offset (a variable font'swghtaxis is the lossless path).fontfetch morph ./Inter.ttf --round=20 --width=108 fontfetch morph ./Geist.otf --slant=8 --rename "Geist Sketch"- Licensing is the gate, not a footnote. OFL fonts get the clean path (Reserved Font Names are renamed automatically). Commercial / unknown-license inputs are allowed but warned about, watermarked in the binary, renamed, and written as a
MOCKUP_bundle with a disclaimer — prototype use only. SetFONTFETCH_MORPH_POSTURE=ofl-onlyto refuse anything not self-declared OFL. Honour the people who make type. - New
@fontfetch/morphpackage carries the engine. It's bundled into the publishedfontfetchCLI (along with@fontfetch/coreandopentype.js), so there's nothing extra to install —npx fontfetchships the whole thing. - Interface-first package boundary. Internal
@fontfetch/inspect,@fontfetch/subset, and@fontfetch/fallbackfacades re-export their surface from@fontfetch/coreto mark a future import boundary. They're not separately published — the single bundled CLI stays the install, so you get the whole package, not à-la-carte pieces.
Accepts TTF / OTF / WOFF / WOFF2 input — WOFF2 is decompressed in and recompressed out transparently. The webapp morph editor (v1.6) and a preset library (v1.7) build on this engine.
Eight features in one minor. Four close out the engine work (competitor-gap closeouts from the 2026-05-28 research) and four ship as distribution channels so fontfetch shows up where users already work.
@fontfetch/registry— new typed npm package. Consumes the community pairings registry with full autocomplete:npm install @fontfetch/registry
import { findByFamily, freeAlternativesFor } from '@fontfetch/registry'; freeAlternativesFor('Söhne'); // ['Inter', 'Manrope', 'Outfit']
fontfetch-actionGitHub Action (extensions/github-action/). PR comments on font drift; non-zero exit when budgets bust or commercial faces sneak in.- Raycast extension (
extensions/raycast/). Three commands: extract fonts from a URL (CSS to clipboard), audit a URL (HUD verdict), search the pairings registry. - Homebrew Formula (
extensions/homebrew/). Source-of-truth tap Formula ready to publish tohomebrew-fontfetchwhen warranted. --gdpr-reportflag. EmitsGDPR.md+gdpr.jsonlisting every third-party font request with self-host remediation. Post-LG München I 20 O 1393/21 (2022) German court ruling on Google Fonts CDN.- Variable-font collapse hint. When a family ships both a variable binary and ≥ 2 static weight files, fontfetch surfaces a one-liner with the byte saving.
fontfetch diff <urlA> <urlB>— staging-vs-prod font drift. Runspull()on both URLs, prints added / removed / shared families with byte and commercial delta.--jsonfor CI:fontfetch diff https://staging.acme.com https://acme.com fontfetch diff https://staging.acme.com https://acme.com --json
fontfetch audit <url>— CI release gate. Non-zero exit on configured rule violations. Combine--max-kb,--per-family-kb,--no-commercial. Pairs with--jsonfor downstream tools:fontfetch audit https://acme.com --max-kb 200 --no-commercial fontfetch audit https://acme.com --per-family-kb Inter:50,Geist:30 --json
fontfetch budget <url> --max-kb N— bundle-size budget shortcut. Same engine asauditwith only the size dimension wired. Drop-in for size-limit / Lighthouse-CI workflows.--emit tokens— W3C / DTCG design tokens. New emitter alongsidenext/tailwind/vite. Writesfonts.tokens.jsonwith W3C Design Tokens Community Group (tr.designtokens.org/format/) entries for every family + weight, plus a Tailwind-aligned size + line-height ladder. Drop into Style Dictionary, Tokens Studio for Figma, or Specify:fontfetch https://vercel.com --emit tokens
CONSISTENCY.mdcross-page report. When--pages > 1, fontfetch writes a per-pull report of shared-vs-divergent families across crawled pages. "Homepage uses Inter;/bloguses Tiempos" — the report names the divergence per page. No competitor does this.- Per-weight Capsize fallback metrics.
--fallbacknow emits one<Family> Fallbackblock per (family, weight, style) tuple, each with matchingfont-weightandfont-styledeclarations. Beatsfontaineon their core feature (fontaine #53, open 3+ years). provenance.jsonmachine-readable license + provenance. Stable v1.0 schema. Shipped per pull alongsideLICENSE_REVIEW.md. Consumed byfontfetch audit, the upcomingfontfetch-actionGitHub Action, and external CI tools.
No new runtime dependencies; bundle size unchanged at ~2.2 MB.
Three additions that round out the subsetting pipeline. After v1.3, fontfetch takes a URL → folder, splits per Google Fonts language bucket, and runs entirely on Node — no Python required:
--formats=woff2modern-only emit. Restricts the kept faces and downloaded files to a chosen format allowlist (one or more ofwoff2,woff,ttf,otf,eot). Addresses a long-standing community ask for modern-format-only output. Halves the typical bundle size on a modern-browser-only site:fontfetch https://shinobidata.com --formats=woff2
fontfetch subset --whitelist=U+00A0,U+20AC— extra codepoints to always keep. Same syntax as a CSSunicode-range. Pairs with the existing DOM-scrape pipeline so glyphs not rendered on page load (currency variants, breaking-space, icon-font glyphs injected by JS) stay alive in the subset. The0xshorthand is also accepted:fontfetch subset https://stripe.com --whitelist=U+00A0,U+20AC,U+0020-007F
fontfetch subset --split-ranges— Google-Fonts-style per-language emit. For every downloaded font, fontfetch intersects its character set against the canonical Google Fonts buckets (latin,latin-ext,cyrillic,cyrillic-ext,greek,greek-ext,vietnamese) and emits one woff2 per bucket plus a chainedfonts.subset.csswithunicode-range:declarations. The output is interchangeable with Google Fonts' owncss2payload for a multi-script family. Browsers lazy-load only the buckets they need at runtime:fontfetch subset https://stripe.com --split-ranges fontfetch subset https://stripe.com --split-ranges=latin,latin-ext,vietnamese
No new runtime dependencies. The split flow reuses the existing fontkit runtime dep (already used by inspect and --fallback) and the subset-font peer dep.
Four small additions targeting the most common confusing outcomes of the v1.2 release:
- Variable fonts now announce themselves. After downloads complete fontfetch inspects every binary on disk; if any expose variation axes you get a one-line notice (
ℹ One variable font detected: Saans (wght 300..900, ital 0..10). All weights and italic styles live in this single binary.) so1 unique file(s)stops reading as "the rest are missing." - Next.js
next/fontsubset siblings. Any URL matching_next/static/media/<hash>-s.<letter>.<ext>triggers an alphabet-wide HEAD probe in parallel and adds the responders to the bundle. Captures the full multi-language family even when the visited page only loaded one unicode subset. --pages <N>multi-page crawl. Visits up toN-1same-origin internal links from the entry HTML (deduped, hash-stripped, asset-extension-skipped) and merges every page's@font-facerules. Solves the "homepage loads Inter but/bloguses Tiempos" problem. Capped at 50; default 1.- Focused empty-state output. Zero
@font-facedeclarations now prints a 3-line "this is usually fixable" frame with concrete next-step flags, instead of a single buried sentence.
fontfetch https://acme.com --pages=5Three subcommands shipped together so the whole pipeline becomes extract → inspect → ship:
-
fontfetch inspect <file>— terminal-native font inspector. Reads any woff2/woff/ttf/otf and prints a column-aligned report: glyph count, format, units-per-em, variation axes, OpenType features, vendor / designer / copyright, and an SIL OFL detection that flags the Reserved Font Name (RFN) clause when present. Wakamai Fondue, but in your terminal. -
--fallback— for every extracted family, fontfetch now reads the binary's metrics via capsize and emits a<Family> Fallback@font-faceblock withsize-adjust/ascent-override/descent-override/line-gap-overridematched to a system fallback (Arial / Times New Roman / Courier New, picked by family-name heuristic). The emittedfonts.csschains'<Family>', '<Family> Fallback', <generic>so the browser swaps between visually identical boxes during the font load. Solves the same CLS problemnext/fontandfontainesolve — but framework-agnostic, plain CSS only. -
fontfetch subset <url>— runs the full extraction, then loads the page in headless Chromium, walks every visible text node plus::before/::aftercontent, and subsets each font down to the unique codepoints actually rendered. Usessubset-font(a WASM wrapper around harfbuzzjs) so it runs pure-Node — no Pythonfonttoolsinstall required, unlikeglyphhanger. Outputs siblings as<original>.subset.woff2. Common case: 30-90% smaller webfonts in one command.
Also bundled in 1.2: every emitted @font-face now defaults to font-display: swap, and fonts.css carries a copy-pasteable <link rel="preload" as="font" type="font/woff2" crossorigin> hint header so you don't have to remember the crossorigin attribute (the most common preload foot-gun).
subset needs subset-font (optional peer dependency, the harfbuzzjs WASM wrapper):
npm install subset-fontEvery pull writes LICENSE_REVIEW.md alongside the rest of the per-site output. Each face is classified by a URL-signature heuristic (Adobe Typekit, Monotype, Hoefler, Type Network, etc.) plus a family-name fallback against a curated SIL OFL / Google Fonts catalog snapshot.
→ License review: 8 open / 2 commercial / 3 unknown
Fail-fast. When every detected font is served from a known commercial-foundry CDN, fontfetch aborts before downloading and emits only LICENSE_REVIEW.md. Pass --force to download anyway (e.g. for a local mockup of a site whose fonts you've licensed).
fontfetch https://commercial-foundry-site.com # aborts, writes LICENSE_REVIEW.md
fontfetch https://commercial-foundry-site.com --force # downloads anywayNot legal advice. The classifier is heuristic-only and conservative on purpose — verify before shipping.
Pass --emit <target,target,...> to generate framework-ready config files alongside the default fonts.css.
fontfetch https://vercel.com --emit next,tailwindTargets:
| Target | Emits | Use it for |
|---|---|---|
next |
next.fonts.ts |
Drop-in next/font/local config — one localFont call per family with all weights, plus a CSS variable |
tailwind |
tailwind.fonts.ts |
fontFamily snippet for tailwind.config.ts — sans / serif / mono heuristic + per-family aliases. Pairs with next for CSS variables |
vite |
vite.fonts.md |
Copy-paste integration guide. Vite needs no plugin — the default fonts.css is already a drop-in stylesheet |
css |
(default) | Explicit no-op |
Output ends up alongside the rest of the per-site bundle:
downloaded-fonts/vercel-com/
├── files/
├── fonts.css
├── fonts.json
├── README.md
├── next.fonts.ts ← --emit next
└── tailwind.fonts.ts ← --emit tailwind
By default fontfetch is static — it fetches the HTML, reads every linked stylesheet and inline <style>, and parses @font-face rules. That covers ~90% of real-world sites and is fast.
For SPAs that load fonts at runtime, sites that inject @font-face blocks via JavaScript after hydration, or pages behind a Cloudflare challenge, pass --headless. fontfetch will launch a headless Chromium via Playwright, wait for document.fonts.ready, and dump every @font-face rule it can see — merged with the static results.
Install Playwright + Chromium once:
npm install playwright
npx playwright install chromiumThen:
fontfetch https://example.com --headlessPlaywright is an optional peer dependency — install it only if you need this mode. The static path runs with zero runtime dependencies.
- Fetches the page HTML
- Pulls every
<link rel="stylesheet">and inline<style>block - Parses every
@font-faceblock: family, weight, style, unicode-range, src - Also grabs
<link rel="preload" as="font">references - Downloads every unique font file
- Rewrites the
@font-faceblocks with local./files/...URLs - Emits
fonts.css,fonts.json, and aREADME.md
No browser launched, no dependencies pulled at install time outside of TypeScript build tooling. The whole CLI is one small ESM bundle.
| Tool | Any URL | JS-rendered fonts | License classify | Framework emit | Inspect | Subset | Per-language split | Modern-only | Zero-CLS fallback | CI release-gate | Cross-page |
|---|---|---|---|---|---|---|---|---|---|---|---|
google-webfonts-helper |
Google only | n/a | n/a | ✗ | ✗ | ✗ | ✓ (Google catalog only) | ✓ | ✗ | ✗ | ✗ |
webfont-dl |
needs CSS URL | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
glyphhanger |
✓ (Puppeteer) | ✓ | ✗ | ✗ | ✗ | ✓ (Python fonttools) |
partial (unicode-range computed) | partial | ✗ | ✗ | ✗ |
fontaine |
n/a | n/a | n/a | partial | ✗ | ✗ | ✗ | n/a | ✓ family-wide (Nuxt/Vite only) | ✗ | ✗ |
fontkit |
library, not a CLI | n/a | partial | ✗ | partial (library) | ✗ | ✗ | n/a | ✗ | ✗ | ✗ |
| Chrome extensions | ✓ (manual) | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
fontfetch |
✓ | ✓ | ✓ | ✓ next/tailwind/vite/tokens (v1.4) | ✓ | ✓ (Node, no Python) | ✓ Google Fonts buckets (v1.3) | ✓ --formats=woff2 (v1.3) |
✓ per-weight, framework-agnostic (v1.4) | ✓ audit / budget / diff / --json (v1.4) |
✓ CONSISTENCY.md via --pages (v1.4) |
"CI release-gate" means non-zero exit codes on rule violations + --json output for downstream tooling. "Cross-page" means crawling multiple pages from a single entry URL and surfacing typography drift between them. Both are categories with zero competitors today.
- v0.1 — Static
@font-faceextraction, ready-to-use CSS, manifest, README - v0.1.1 — Community font-pairing registry: share what fonts your favorite sites use, with free OFL alternatives
- v0.2 —
--headlessflag: Playwright mode for JS-loaded fonts (Adobe Typekit, SPAs, Cloudflare-protected sites) - v0.2.2 — Referer-aware font downloads (unblocks foundry CDNs that 403 without a Referer)
- v0.3 — Framework emitters:
--emit next/tailwind/vite - v0.4 — License heuristic +
LICENSE_REVIEW.md+ fail-fast on all-commercial sites (--forceto bypass) - v0.6 — Provenance grouping: output split into
google//adobe-typekit//commercial//open-cdn//self-hosted/ - v1.0 — pnpm-workspaces monorepo restructure:
@fontfetch/core+ the CLI, withapps/slots reserved for the webapp and headless worker - v1.2 — Inspect + subset + fallback release:
fontfetch inspect(terminal Wakamai Fondue),--fallback(zero-CLS@font-faceblocks via capsize),fontfetch subset(Playwright DOM scrape + harfbuzzjs subset, no Python). Plusfont-display: swapdefault and preload-hint header on every emittedfonts.css. - v1.2.1 — Discovery + empty-state quick wins: variable-font hint after pull, Next.js subset sibling probe,
--pages <N>multi-page crawl, focused 0-declaration output. - v1.3 — Modern emit + whitelist + per-language split:
--formats=woff2modern-only emit,subset --whitelist=U+00A0,…extra codepoints,subset --split-rangesGoogle-Fonts-style per-language woff2 + chainedfonts.subset.csswithunicode-range:declarations. - v1.3.1 — Signal quality:
--fallbackreadspost.isFixedPitch(catches Operator / PragmataPro / Berkeley Mono); license classifier cross-references the binary'snametable (ids 13 + 14);LICENSE_REVIEW.mdcalls out OFL Reserved Font Name families. - v1.4 — CI release-gate + distribution channels: engine =
fontfetch diff/audit/budget+--emit tokens+--gdpr-report+ per-weight Capsize fallback + cross-pageCONSISTENCY.md+ machine-readableprovenance.json+ variable-font collapse hint. Channels =@fontfetch/registrytyped npm package +fontfetch-actionGitHub Action + Raycast extension + Homebrew tap. - v1.5 — Prototype-grade font morphing:
fontfetch morph <file> --round --width --slant --weight --rename. Pre-commission sketchbook for designers — four sliders, real binary out, OFL-rename-enforced. Webapp/edit/[id]with live preview + share-to-client links lands in v1.6; community preset library in v1.7. - v0.5 — Hosted webapp at
fontfetch.dev: URL → live progress → foundry-style previews → compare + pairing
Want one of these sooner? Open an issue or vote on existing ones.
Font files are software, licensed under EULAs. fontfetch is intended for local design exploration and testing, not for shipping paid fonts you haven't licensed. Using a font for a few hours of mockup work in a private project is different from bundling it into a production app. We don't gate the tool — we trust you to know the difference and respect foundry licenses.
For production use, the Google Fonts catalog and the SIL Open Font License library are designed to be self-hosted freely. Every entry in our pairings registry lists free alternatives for paid fonts.
pairings/ is a community-curated list of fonts used by real websites — with free OFL alternatives for every commercial font.
→ Submit a pairing (fill a form, drag a screenshot, done — or ask an AI to do it for you).
Issues and PRs welcome. See CONTRIBUTING.md for the dev loop. The codebase is small and approachable — a pnpm-workspaces monorepo with two packages (@fontfetch/core and the published fontfetch CLI), and apps/ slots reserved for the v0.5 webapp and headless worker. tsup bundles core into the CLI so npm consumers see one self-contained file.
Good first issues are tagged good first issue on GitHub.
MIT — © Niyam Vora