How to generate a type scale in ci from the runner api
- Step 1Discover the option schema — `GET http://127.0.0.1:9789/v1/tools` (or the hosted `GET /api/v1/tools/typography-scale-builder`) returns the tool's options: `baseSize`, `scaleRatioName`/`scaleRatio`, `steps`, `scaleOrigin`, `fluidViewportMin`, `fluidViewportMax`. Use this to validate your CI inputs against the real contract instead of guessing.
- Step 2Pair the runner once — Install and pair the JAD runner on the build machine. It exposes the same font engine the browser tool uses at `http://127.0.0.1:9789`. After pairing, the type-scale endpoint is available with no per-call auth dance beyond the runner's local token.
- Step 3POST the scale options — Send `{ "options": { "baseSize": 16, "scaleRatio": 1.25, "steps": 8, "scaleOrigin": 2, "fluidViewportMin": 320, "fluidViewportMax": 1280 } }` to `/v1/tools/typography-scale-builder/run`. The response carries the generated CSS (`text/css`). Pass `scaleRatio` as a number for ratios the dropdown lacks.
- Step 4Write the CSS artifact — Save the returned content as `tokens/type-scale.css` in your repo. Commit it (or generate it fresh each build). Because identical inputs give identical output, a no-op build produces no diff — handy as a drift check in CI.
- Step 5Parse the numbers if you need other platforms — The tool only emits CSS. To feed iOS/Android, extract the rem (and the px from the trailing comment) with a small regex, or just recompute `base × ratio^(i-origin)` yourself — the math is trivial and matches the tool exactly. Feed those numbers into Style Dictionary or your own transformer.
- Step 6Gate the build on the artifact being current — Re-run the generation in CI and `git diff --exit-code` the output. If someone changed the base or ratio in the config without regenerating, the diff is non-empty and the build fails — keeping the committed scale and the config in sync.
Runner API options for the type-scale builder
The full set the endpoint accepts. The three marked 'web UI: no' are honoured by the API but absent from the on-page form.
| Option | Type | Default | Web UI? |
|---|---|---|---|
baseSize | number (10–32) | 16 | Yes |
scaleRatioName | enum of 8 names | major-third | Yes (dropdown) |
scaleRatio | number | 1.25 | No (derived from the name in UI) |
steps | number (3–12) | 8 | Yes (slider) |
scaleOrigin | number (0–8) | 2 | No |
fluidViewportMin | number (240–1024) | 320 | No |
fluidViewportMax | number (800–2400) | 1280 | No |
Output reality vs. common assumptions
What the tool returns, and what you must build yourself. Be honest in your pipeline docs so teammates do not expect features that are not there.
| Asset | From this tool? | How to get it |
|---|---|---|
CSS :root rem scale | Yes | Directly in the response — first :root block. |
Fluid clamp() CSS scale | Yes | Second :root, inside @media (min-width: <min>). |
| px values | Yes (as comments) | Trailing /* Npx */ on each static line, or recompute. |
| W3C Design Tokens JSON | No | Parse the CSS or recompute, then emit JSON yourself. |
| iOS Swift / UIKit points | No | Transform the rem/px in Style Dictionary or a script. |
Android dimens.xml (sp) | No | Same — transform the numbers; the tool does not emit XML. |
Cross-platform conversion of the same numbers
Once you have the canonical px from the tool, the per-platform conversions are arithmetic. Example for lg = 20px at a 16px base.
| Platform | Unit | Conversion | lg value |
|---|---|---|---|
| Web | rem | px / baseSize | 1.25rem |
| Web (fluid) | clamp() | tool output | clamp(1.063rem, 0.938rem + 0.625vw, 1.438rem) |
| iOS | pt | px (1pt ≈ 1px at @1x) | 20pt |
| Android | sp | px (density-independent) | 20sp |
Cookbook
Real CI recipes. Each calls the runner endpoint and post-processes the CSS — the tool's only output format. To complete the typography pipeline, pair these with the font-face generator and preload tag builder in the same prebuild.
curl the runner and write the CSS
ExampleThe minimal CI step: POST options, save the CSS. Works in any image with curl; no Node, Python, or native font libs required.
curl -s -X POST http://127.0.0.1:9789/v1/tools/typography-scale-builder/run \
-H 'Content-Type: application/json' \
-d '{"options":{"baseSize":16,"scaleRatio":1.25,"steps":8}}' \
-o tokens/type-scale.css
# tokens/type-scale.css now holds the static + fluid blocksNode prebuild that calls the runner
ExampleDrop into a package.json prebuild. Fetches the scale and writes it where the bundler will pick it up. Same engine as the browser, no extra deps.
// scripts/gen-type-scale.mjs
import { writeFile } from 'node:fs/promises';
const res = await fetch('http://127.0.0.1:9789/v1/tools/typography-scale-builder/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ options: { baseSize: 16, scaleRatio: 1.333, steps: 9, scaleOrigin: 2 } }),
});
const css = await res.text();
await writeFile('src/styles/type-scale.css', css);
console.log('wrote type-scale.css');Extract the px values into a tokens JSON
ExampleThe tool gives CSS; if your pipeline wants JSON tokens, parse them out. This regex reads the static block's name + px comment. From here you can feed Style Dictionary.
// from the generated CSS text
const re = /--font-size-([\w]+):\s*[\d.]+rem;\s*\/\*\s*([\d.]+)px/g;
const tokens = {};
for (const [, name, px] of css.matchAll(re)) {
tokens[name] = { value: `${px}px`, type: 'fontSize' };
}
// tokens = { xs:{value:'10.24px'...}, sm:{...}, base:{...}, ... }
// write tokens.json and run Style Dictionary's transformsRecompute instead of parsing (no runner needed)
ExampleIf you would rather not call the runner, the math is one line and matches the tool's rounding exactly (px rounded to 2 dp). Useful for pure-static-site generators.
const LABELS = ['xs','sm','base','lg','xl','2xl','3xl','4xl','5xl','6xl','7xl','8xl'];
function scale(base = 16, ratio = 1.25, steps = 8, origin = 2) {
return Array.from({ length: steps }, (_, i) => {
const px = Math.round(base * ratio ** (i - origin) * 100) / 100;
return { name: LABELS[i] ?? `step-${i}`, px, rem: +(px / base).toFixed(3) };
});
}
// scale() === the tool's static block, value for valueDrift check: fail CI if the committed CSS is stale
ExampleRegenerate in CI and diff against the committed file. A non-empty diff means the config and the artifact disagree — fail the build so the scale stays the single source of truth.
node scripts/gen-type-scale.mjs if ! git diff --quiet src/styles/type-scale.css; then echo 'type-scale.css is out of date — regenerate and commit' git --no-pager diff src/styles/type-scale.css exit 1 fi
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.
You expected JSON / Swift / Android output from the endpoint
By designThe endpoint returns text/css and nothing else — the same type-scale.css the browser produces. There is no format=json or platform=ios parameter. For other platforms, parse the rem/px from the CSS (cookbook) or recompute from base × ratio, then run your own transformer. Document this in your pipeline so teammates do not look for a flag that does not exist.
Runner not paired or not running on the build agent
Connection refusedhttp://127.0.0.1:9789 only responds if the JAD runner is installed and paired on that machine. In ephemeral CI agents you must start/pair it as a setup step, or fall back to the recompute-locally recipe (no runner needed). A connection-refused error means the runner is not up — check the setup step before the generation step.
You passed scaleRatioName but want a number not in the enum
Watch outscaleRatioName only accepts the eight known names; an unknown name falls back to the default. To use an arbitrary ratio (e.g. 1.27), send the numeric scaleRatio field instead — the handler reads scaleRatio directly. Sending both is fine; the numeric one is what the math uses.
Changing baseSize shifts the clamp() slope unexpectedly
Watch outThe fluid clamp() uses baseSize as the root font size for its rem/vw math. If you generate at base 18 but your site's html is 16px, the fluid values render off from the px comments. Keep baseSize and your actual root font-size aligned, or generate at 16 and scale via the ratio.
Steps above 8 in CI produce 5xl–8xl tokens you did not expect
ExpectedThe label list runs to 8xl (12 names). A steps: 12 run emits all of them. If your downstream config only knows xs–4xl, the extra tokens are silently unused but still in the CSS. Either trim the steps or extend your consuming config to map the larger tokens.
Regex parse misses the base line's odd spacing
Watch outThe static block uses irregular padding before the px comment (it is cosmetic). Anchor your regex on --font-size-NAME: and the rem; plus the /* Npx */ comment rather than fixed column positions. The cookbook regex is tolerant of the spacing; copy it rather than hand-rolling a brittle one.
px values are rounded, so recompute and parse can differ in the last digit
ExpectedThe tool rounds px to two decimals before dividing for rem (3 dp). If you recompute with full precision you may see 1.5625 where the tool shows 1.563. For pixel rendering this is irrelevant, but if you assert equality in tests, round your recomputed values the same way the tool does (Math.round(px*100)/100).
No file is uploaded, yet you wired a file field
By designThis is a generative tool — needsFile is false. The runner endpoint expects only an options object; do not send a multipart file. If your generic runner wrapper always attaches a file, the type-scale call will still work but the file is ignored. Keep the request to { options: {...} }.
You want element rules (h1{...}) generated for you
By designThe output is variables only — no h1 { font-size: var(--font-size-4xl) } rules. Generate those in your own template after writing the CSS. This keeps the artifact framework-neutral. The mapping example in the online builder guide shows a sensible default assignment.
Frequently asked questions
Can I really run the type-scale builder in CI?
Yes — through the JAD runner's local HTTP API. POST http://127.0.0.1:9789/v1/tools/typography-scale-builder/run with an options object returns the same CSS the web page produces. The runner runs the identical font engine locally, so there is no network round-trip to JAD's servers for the generation itself.
Does the tool output design tokens JSON?
No. It emits a single CSS file (text/css) with a static rem scale and a fluid clamp() scale. If you need JSON tokens, parse the rem/px values out of the CSS (the cookbook has a regex) or recompute them from base × ratio, then write your own JSON and run Style Dictionary or similar.
Can it generate iOS and Android sizes directly?
No — there is no Swift or dimens.xml output. Use the px values from the tool as the canonical numbers and convert them: iOS points are effectively the px value, Android sp likewise. Do the conversion in Style Dictionary or a small script. The tool's job is the math and the CSS; cross-platform fan-out is yours.
How do I use a ratio that is not in the dropdown?
Send the numeric scaleRatio field to the runner API instead of scaleRatioName. The handler reads scaleRatio directly, so any positive number works — 1.27, 1.35, whatever your brand needs. The web form only offers the eight named presets; the API has no such limit.
What options does the API accept that the web page doesn't?
scaleOrigin (where the base sits in the steps), fluidViewportMin, and fluidViewportMax. The on-page form omits all three, so browser runs use origin 2 and a 320→1280px fluid range. The API honours whatever you pass — see the options table above.
Is the output deterministic enough to commit?
Yes. Identical options always produce byte-identical CSS. That makes a drift check trivial: regenerate in CI and git diff --exit-code. A non-empty diff means the committed scale and the config disagree, and the build fails. The cookbook has the exact snippet.
Do I need a font file for this?
No — it is generative (needsFile: false). The runner request is just { options: {...} }; no multipart upload. Because there is no file, the tier file-size limits never apply to this tool.
What if the runner isn't available in my CI image?
Two options: (1) install and pair the runner as a CI setup step so the endpoint is up before generation; or (2) skip the runner entirely and recompute the scale with the one-liner in the cookbook — the math (base × ratio^(i-origin), px rounded to 2 dp) matches the tool exactly, so you get identical numbers with no service dependency.
How do I parse the px out of the CSS reliably?
Anchor a regex on --font-size-NAME: plus the rem; and the trailing /* Npx */ comment, not on fixed column positions (the spacing is cosmetic and irregular). The cookbook regex handles it. From the captured name and px you can build any downstream token format.
Can I fan this out to Style Dictionary?
Yes — but Style Dictionary consumes tokens, not CSS, so first parse the tool's CSS (or recompute) into a tokens object, then point Style Dictionary's css, ios, and android transform groups at it. The tool produces the source numbers; Style Dictionary produces the per-platform files.
Does generation send my scale config to JAD's servers?
When you use the runner endpoint (127.0.0.1:9789), the computation happens locally on your machine — nothing about your scale leaves it. The hosted GET /api/v1/tools/... endpoint is only for fetching the option schema, not for processing.
What else should be in the same prebuild?
A full typography prebuild often pairs the scale with the font-face generator for the @font-face blocks, the preload tag builder for critical weights, and the system font stack generator for the fallback stack. All four are generative and runner-callable, so one script can emit your entire type foundation.
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.