URL parameters
The complete v2 parameter list. Anything not in this table (subset, effect, sans-serif aliases) belongs to the deprecated v1 /css endpoint and is silently ignored or rejected by /css2.
| Parameter | Required | Values | Notes |
|---|---|---|---|
family | Yes | Name or Name:axes@tuples | Repeat to request multiple families in one response |
display | No | auto, block, swap, fallback, optional | Applied to every @font-face in the response |
text | No | URL-encoded UTF-8 string | Specify once per request; applies to all families; cuts response to needed glyphs only |
font-display values
Controls the FOIT/FOUT trade-off while the WOFF2 is downloading.
| Value | Block period | Swap period | When to use |
|---|---|---|---|
auto | Browser default | Browser default | Skip unless you have a strong reason — behaviour varies |
block | ~3s invisible text | Infinite | Headings where the brand typeface matters more than instant text |
swap | 0s | Infinite | Most body copy — fallback shows instantly, swaps on load |
fallback | ~100ms | ~3s | Performance-sensitive body copy; gives up if the font is slow |
optional | ~100ms | 0s | Only use the font if it's already cached; never blocks first paint |
Common variable-font axes
Standard registered axes use lowercase tags; custom foundry axes use uppercase. Not every family exposes every axis — check the Type Tester on the family's Google Fonts page.
| Tag | Name | Typical range | Example |
|---|---|---|---|
wght | Weight | 100..900 | Inter:[email protected] |
wdth | Width | 75..125 | Roboto+Flex:[email protected] |
ital | Italic | 0 or 1 (discrete) | Inter:ital,wght@0,400;1,400 |
opsz | Optical size | 8..144 | Roboto+Flex:[email protected] |
slnt | Slant | -10..0 (degrees) | Roboto+Flex:[email protected] |
GRAD | Grade | -200..150 | Roboto+Flex:[email protected] |
MONO | Monospace | 0..1 | Recursive:[email protected] |
CASL | Casual | 0..1 | Recursive:[email protected] |
Response format by User-Agent
The CSS the API returns is identical in shape, but the src URL inside each @font-face points to a different format depending on what the client claims to support. Set User-Agent deliberately when fetching from Node, curl, or a build script — the build-pipeline guide wires this into a CI script that downloads every WOFF2 referenced by the CSS.
| Client | src format | Why |
|---|---|---|
| Modern Chrome / Firefox / Safari / Edge UA | WOFF2 | Smallest payload — every supported browser since 2018 |
| Old Safari / Android stock browser UA | WOFF | Pre-WOFF2 era browsers |
| IE 9 UA | EOT | Legacy Microsoft format |
| IE 6–8 UA | TTF (truetype) | Pre-WOFF browsers |
| curl / fetch with no UA or unknown UA | TTF (truetype) | Safest universal fallback — but ~3× larger than WOFF2 |
Cookbook
Copy-pasteable URLs for the most common requests. If you'd rather not memorise the grammar, paste any of these into the decoder above to see the breakdown — or use the generator below to build one from a UI. For the full self-hosted alternative discussed throughout, see the self-hosting comparison.
One family, one weight, swap
ExampleThe minimum viable URL. Use this for a single-weight headline font.
https://fonts.googleapis.com/css2?family=Inter:wght@600&display=swap
Two specific weights
ExampleRegular and bold for body copy. Tuples separated by semicolons.
https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap
Full variable weight range
ExampleOne @font-face block with the variable WOFF2 covering every weight from thin to black. Costs about the same bytes as two static weights.
https://fonts.googleapis.com/css2?family=Inter:[email protected]&display=swap
Roman and italic, three weights each
Exampleital sorts before wght alphabetically, so it comes first. Tuples lead with 0 (upright) or 1 (italic). Six @font-face blocks total: three weights × two ital states.
https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap
Two families, one request
ExampleA heading and body pair fetched in a single HTTP round-trip. Saves a DNS lookup and TLS handshake versus two separate links.
https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Roboto+Mono:wght@400&display=swap
Two-axis variable font
ExampleRoboto Flex exposes both optical size and weight. Note opsz comes before wght (alphabetical). Tuple order matches axis declaration order.
https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,[email protected],100..1000&display=swap
Subset to specific text
ExampleHero string that never changes? Tell Google exactly which glyphs you need and download ~90% less. URL-encode the value.
https://fonts.googleapis.com/css2?family=Playfair+Display:wght@900&text=Welcome+home&display=swap
Node fetch — with the User-Agent that gets WOFF2
ExampleWithout the User-Agent header, Node fetch / undici / curl gets TTF and no unicode-range subset blocks (3–5× larger payload, no per-script tree-shaking). Set a recent Chrome UA and you get the same WOFF2 + multi-subset response a browser sees.
const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/123.0.0.0 Safari/537.36'; const res = await fetch( 'https://fonts.googleapis.com/css2?family=Inter:[email protected]&display=swap', { headers: { 'User-Agent': ua } }, ); const css = await res.text();
Benchmarks
Numbers we measured for this page. Every byte count below comes from a real Chrome User-Agent request to fonts.googleapis.com on , with the response WOFF2 fetched and counted directly.
Variable vs discrete weights (Inter, latin subset, modern Chrome UA)
Same family, four ways to request its weights. The headline finding: at 2+ weights Inter is served as a single variable WOFF2 no matter how you ask. Variable-range syntax is a wash on payload but produces a tiny CSS file; spelling out every weight produces an 8× larger CSS for the same fonts.
| Request shape | Latin WOFF2 | CSS size | Wire total |
|---|---|---|---|
Inter:wght@400 (single static) | 23,664 B | 2,590 B | 26,254 B |
Inter:wght@400;700 (2 discrete) | 48,256 B | 4,941 B | 53,197 B |
Inter:wght@100;200;300;400;500;600;700;800;900 (9 discrete) | 48,256 B | 22,238 B | 70,494 B |
Inter:[email protected] (variable range) | 48,256 B | 2,498 B | 50,754 B |
Methodology: GET fonts.googleapis.com/css2 with a Chrome 120 User-Agent on 15 May 2026. CSS Content-Length read from the response; WOFF2 size read from the Content-Length of each unique gstatic.com URL the CSS references for the latin unicode-range. Inter's full multi-script response is ~213 KB; the latin row is shown because that's what most sites actually consume.
`text=` subsetting (Inter weight 400)
The text= parameter is the single biggest size win on this API. Google's docs claim 'up to 90%' — for short strings the reality is steeper. The 36-character pangram is 87% smaller than the default latin subset.
| Request | Unique chars | WOFF2 size | vs default latin |
|---|---|---|---|
text=A | 1 | 968 B | 96% smaller |
text=Hello+World | 8 | 1,448 B | 94% smaller |
text=Welcome+to+JAD+Apps | 13 | 1,940 B | 92% smaller |
text=The+quick+brown+fox+jumps+over+the+lazy+dog | 28 | 2,980 B | 87% smaller |
No text= (default latin subset) | ~230 | 23,664 B | baseline |
Methodology: Same browser/UA setup. Each `text=` URL was fetched, the kit URL extracted, and the WOFF2 body's size measured directly (HEAD doesn't return Content-Length for `gstatic.com/l/font?kit=...`).
Edge cases and what actually happens
Every row below was probed against the live API. Some documented requirements (alphabetical axis order, numerical tuple order) are not actually enforced in practice — useful to know if you've been blaming the wrong thing for a 400.
Axes listed in non-alphabetical order — `Inter:wght,ital@400,0;700,1`
200 OKGoogle's docs say the axis tag list must be alphabetically sorted. In practice the API accepts either order. Sorting alphabetically still matches the docs and avoids future churn if Google tightens the parser, but it is not a current cause of 400s — despite what most guides on the web claim.
Tuples in non-numerical order — `Inter:ital,wght@1,400;0,400`
200 OKSame story. The docs require numerical ordering on tuples; the API doesn't enforce it. Order anyway for readability.
Overlapping ranges — `Inter:[email protected];400..900`
400 Bad RequestThe 'no overlaps' rule **is** enforced. Response: 400: Invalid selector. If you have two ranges that touch (like 100..500 and 500..900), make one of them stop at 499.
Invalid axis tag — `Inter:zzzz@100`
400 Bad RequestUnknown 4-letter tag returns 400: Invalid selector. Mistyping wght as wgth will land here. The decoder above flags this before you fire the request.
Axis the family doesn't expose — `Inter:wdth@100`
400 Bad RequestInter has no width axis. The API returns 400: Missing font family and echoes back the request: Requested: Inter (style: normal, weight: 400, {wdth=100.0}). The wording is misleading — Inter exists; what's missing is the wdth axis. Same error fires if you ask Bebas Neue (single-style) for italic. Treat 'Missing font family' as 'family or this combination of axes does not exist'.
Tuple count mismatch — `Inter:ital,wght@400`
400 Bad RequestDeclared two axes (ital, wght) but the tuple has one value. Each tuple must have exactly one value per declared axis. Response: 400: Invalid selector.
Emoji in `text=` — request a glyph the family doesn't have
200 OK on the CSS, 400 on the WOFF2Worth knowing because the failure is silent. The /css2 endpoint returns a normal-looking @font-face block with font-display: swap and a gstatic.com/l/font?kit=... URL. That URL returns HTTP 400 with an HTML body, not a WOFF2. The CSS validator sees no errors; the browser swaps in the fallback for that character with no console warning. If your text= value includes any code point the family lacks (emoji, CJK glyphs on a Latin-only family), assume the font fetch will fail and design fallbacks accordingly.
Very long `text=` — hard URL byte limit
400 Bad Request above the limitWe binary-searched the boundary on 15 May 2026: the full URL (including https://fonts.googleapis.com/css2?) can be up to **16,412 bytes**, and 16,413 bytes returns a 400. The threshold is on URL bytes, not character count — 16,000 ASCII characters in text= fits, but ~1,816 unique CJK code points overflows because each one URL-encodes to 9 bytes. Inside the limit, Google handles it without complaint: only *unique* characters drive WOFF2 size, so a value with 100 unique glyphs serves the same payload whether it's 100 chars or 10,000.
Frequently asked questions
Why am I getting a 400 Bad Request from the API?
There are two visible error wordings, and which one you get tells you what went wrong. **400: Invalid selector** means the family is fine but the axes/tuples are malformed: overlapping ranges ([email protected];400..900), a tuple count mismatch, mixing discrete and range in one family, or a mistyped 4-letter axis tag. **400: Missing font family** means the family-plus-axes combination doesn't exist: a nonexistent family name, or an axis the family doesn't expose (asking Inter for wdth, or Bebas Neue for italic). What is **not** a cause, despite what most guides claim: axes listed in non-alphabetical order, or tuples in non-numerical order. Both are documented requirements; neither is actually enforced. The decoder above runs the same checks before you fire the request.
What's the difference between `/css` and `/css2`?
/css is the original v1 endpoint. It uses a different query syntax (family=Inter:400,700i, plus subset= and effect= parameters). /css2 is the current API and uses the family=Name:axes@tuples grammar with full variable-font support. New projects should use /css2 exclusively. v1 is still served for backwards compatibility but isn't getting new features.
Does `/css2` support the `subset=` parameter?
No. subset= belongs to the v1 /css endpoint and is silently ignored by /css2. v2 always returns multiple @font-face blocks tagged with unicode-range — one per script (latin, latin-ext, cyrillic, greek, vietnamese, devanagari, etc.). Browsers download only the WOFF2 files whose ranges match characters on the page. For finer control, use text= to request glyphs for a specific string — the benchmarks above show real sizes.
What format does the API return — WOFF2 or TTF?
It depends entirely on the User-Agent header. Modern browser UAs get WOFF2. Generic clients (curl, node-fetch with default UA) get TTF, which is roughly 3× larger. If you're fetching the CSS server-side to inline or self-host, send a recent Chrome User-Agent explicitly or you'll get unnecessarily large font files and no unicode-range subset blocks. The cookbook above includes a Node example with the correct header.
Are discrete weights or a variable range cheaper to request?
Identical WOFF2 payload, different CSS size. From the benchmarks above: requesting 9 discrete weights of Inter and the variable range [email protected] both return the same 48 KB variable WOFF2 — Google deduplicates to the single variable file. But the discrete-weight CSS is 22 KB (one @font-face per weight per subset) versus 2.5 KB for the variable range. If you genuinely only need one weight, a single static weight is half the WOFF2 size at 23 KB.
Can I mix discrete weights and a variable range in one family?
No. Within a single family= parameter you pick one strategy: either discrete tuples (wght@400;700) or a range ([email protected]). Mixing them produces a 400. If you genuinely need both behaviours, repeat the family= parameter with different syntaxes — but in practice the variable range already covers every discrete weight you'd ask for.
How do I request italic without specifying multiple weights?
family=Inter:ital@0;1 returns upright and italic at the default weight. The ital axis is discrete — only 0 and 1 are valid values, not a range. If you specify ital alongside any other axis, every tuple needs a value for every axis.
Why are axis tags case-sensitive?
OpenType axis tags are four-character identifiers. Registered axes (the ones in the OpenType spec) use lowercase: wght, wdth, ital, opsz, slnt. Custom axes registered by foundries use uppercase: GRAD, MONO, CASL, XOPQ. Google preserves this casing in URLs because OpenType requires it — wght and WGHT are different axes.
Is there a rate limit?
Google has never published one. Fonts are served from a global CDN with aggressive caching (responses include long Cache-Control max-age values), so most traffic hits the edge and never reaches an origin that would rate-limit. If a build pipeline is hammering the API enough to notice limits, self-hosting removes the dependency entirely.
Does `font-display` affect the WOFF2 file or just the CSS?
Only the CSS. font-display is a CSS property — Google bakes it into the @font-face block but the WOFF2 binary is identical regardless. You can also override it client-side by re-declaring the @font-face with your own font-display value.
What does a default-weight-only request look like?
family=Inter with no colon, no axes, no @ — just the family name. The response contains one @font-face per script subset at weight 400 (or whatever the family considers its regular). Fine for one-off prototypes; for production, always specify the weights you actually use so the CDN can serve the right variable / static file.
Can I host the CSS file myself?
Yes, and most production sites should. Fetch the CSS server-side (with a modern browser User-Agent), download every WOFF2 it references, rewrite the src URLs to point at your domain, and serve both the CSS and WOFF2 files yourself. This removes the third-party dependency, improves privacy (Google no longer sees your visitors), and often improves performance because there's one origin instead of two. The full build pipeline is in the self-hosted build-pipeline script.
Does the API return a separate WOFF2 per weight or one variable file?
Depends on the request and the family. Discrete single-weight requests (wght@400) get a static WOFF2 (typically half the size of the variable file — 23 KB vs 48 KB for Inter latin). Multi-weight discrete requests on a variable family get deduplicated to the single variable WOFF2 across all @font-face blocks. Variable-range requests ([email protected]) always return the variable WOFF2. Browser cache by URL means there's never more than one WOFF2 fetch even when many @font-face blocks point at the same file.
How much does `text=` actually save?
From the measured table above: a single character is 968 bytes, 'Hello World' is 1.4 KB, the 26-letter pangram is 3 KB. Versus a 23.6 KB default latin subset that's 87–96% smaller. The catch: text= is per-URL, so every time the displayed string changes you need a new fetch. Use it for content that's fixed at build time (hero typography, logos, marketing slogans), not for dynamic content where the strings vary per visit.
Privacy first
Every JAD Font tool runs entirely in your browser using opentype.js and the wawoff2 WASM Brotli encoder. Your fonts never leave your device — verified by zero outbound network requests during processing.