bitbaum Start a project
Package · MIT

design-tokens

One tokens.css behind the products, so a retheme is an edit to one file.

Why it exists

Color, type and radius were being copied into each app and then drifting. A retheme meant a hunt. Dark mode meant a second hunt.

How it works

Primitives, then semantic names, as CSS custom properties. Tailwind points at the variables and holds no literal color of its own. A product themes by overriding the variables, not by forking the components.

Where it fits

It is why the studio can look like one studio. Loki, Solon, Skif, Substrata and this site share it. A package that draws a page is expected to use these tokens rather than invent a palette.

LicenceMIT
Registry releasev1.5.2
Install
Sourcebitbaum/design-tokens
Declared consumers26 repositories

API and examples

Documentation from source commit d8fb5fdaa329 · 2026-11-07. Source documentation can include work beyond the registry release.

Read the package documentation

The design single source of truth for OrangeCat, Loki and Solon.

One file — tokens.css — defines every colour, typeface, weight, tracking, radius and shared primitive the three products use. They import it. Nothing is copied.

Why

These tokens used to be hand-copied into three repos, and they drifted. Solon's globals.css carried a comment claiming its tokens matched OrangeCat's while sharing zero names or values with it — so the three products looked like three different companies. Copies drift; imports cannot.

Retheming the entire stack

  1. Edit the block marked ▼▼▼ THE KNOBS ▼▼▼ in tokens.css for a retheme, or update the optional composition.css contract for interface improvements.
  2. Bump package.json's version and set the matching explicit .github/release-request.txt; submit a PR and pass package CI.
  3. A successful main-branch push CI triggers tag-approved-release.yml. It tags only the exact latest verified SHA and never rewrites an existing version tag.
  4. The tag workflow explicitly dispatches publish.yml; npm publishing is still guarded by NPM_PUBLISHING and requires trusted-publishing bootstrap. If that variable is off, a Git tag exists but no npm release is claimed.
  5. Upgrade consumers to the confirmed Git tag, published npm version, or an exact source-commit SHA; run their frozen-lockfile CI.

The one-time release request does not automatically approve future version bumps: a request must match the manifest version.

Changing the display typeface is one line:

--font-display: "Instrument Serif", Georgia, serif;
--font-display-weight: 400;   /* the face's weight lives WITH the face */
--tracking-display: -0.015em; /* and so does its tracking */

Install

// package.json
"@bitbaum/design-tokens": "^1.2.0"
// app/layout.tsx — import BEFORE the app's own globals.css
import "@bitbaum/design-tokens/tokens.css";
import "./globals.css";
// tailwind.config.js (v3)
module.exports = {
  presets: [require("@bitbaum/design-tokens/tailwind-preset")],
  content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
};

Tailwind v4 apps consume tokens.css through @theme; the variable names are identical, so both paths stay in sync.

Optional cross-product composition grammar

The brand tokens above intentionally belong to OrangeCat/Loki/Solon. Their shared palette should never be imposed on unrelated products or customer websites.

For spacing, control sizing, visual rhythm and interaction decisions, composition.css is an opt-in, namespaced layer. It defines no fonts, brand colours, body style or global reset.

// App Router — load AFTER the app's CSS reset and BEFORE product CSS modules.
// The shared classes intentionally use :where() specificity, so importing
// them before a later universal '*' padding reset would erase their padding.
import './globals.css';
import '@bitbaum/design-tokens/composition.css';
<main className="bb-frame">
  <div className="bb-stack" style={{ '--bb-stack-gap': '1.5rem' }}>
    <div className="bb-measure">A product-specific headline and description</div>
    <div className="bb-actions">
      <a className="bb-button bb-button--primary" href="/start">Get started</a>
      <a className="bb-button bb-button--secondary" href="/method">Read the method</a>
    </div>
  </div>
</main>

Map only the small seam of semantic CSS roles once in each app:

:root {
  --bb-action-bg: var(--color-action);
  --bb-action-fg: var(--color-on-action);
  --bb-control-bg: var(--color-surface);
  --bb-control-fg: var(--color-text);
  --bb-line: var(--color-border);
  --bb-muted: var(--color-text-muted);
  --bb-focus: var(--color-focus);
}

The exported grammar includes bb-frame, bb-measure, bb-section, bb-stack, bb-cluster, bb-grid, bb-actions, bb-choice-row, bb-list, bb-keyline, bb-tabular, bb-control, bb-button, and bb-skip.

It guarantees 44px controls, visible focus, responsive equal-cell choices, and reduced-motion support. Apps own typography, colours, spacing adjustments, components and their content. Do not render all information as cards: the grammar deliberately offers whitespace and hairline lists.

Release contract: the new composition subpath first appears in source version 1.2.0; v1.1.0 does not include it. The verified tagging pipeline only tags a version explicitly requested in .github/release-request.txt after main CI is green. Verify the v1.2.0 Git ref and the registry separately before claiming either is published. Until then, use a verified exact commit SHA. Existing tokens.css consumers do not change appearance by opting out.

The contract

Components declare intent, never mechanics.

DoDon'tWhy
`className="font-display"``font-['Instrument_Serif']`the face is not a component's business
`className="font-display"``font-display font-bold`the display face has **one weight**; a synthesized bold looks cheap
`className="bg-surface-base"``bg-[#1c1c1c]`retheming must not require finding hex codes
`className="text-accent"``text-[#ff5c00]`one action colour, one definition
`bg-accent text-on-accent``bg-accent text-white`white on the accent is **3.10:1** and fails AA; the paired ink is 6.10:1
`className="tabular"`—numbers that change in place must not jitter
`font-display` at ≥`text-2xl``font-display text-lg`below 24px the high-contrast serif reads *lighter* than the body sans and hierarchy inverts
`className="wordmark"``font-display` on the logouppercase + open tracking is the one small size a display serif survives

Enforce it in each repo's design gate so drift fails the build rather than shipping quietly.

Typefaces

Self-hosted (latin + latin-ext), no runtime requests to Google:

RoleFaceWeightsCarries
displayInstrument Serif400 onlyheadlines, wordmarks
sansInter400–700 variableall UI and body text
monoIBM Plex Mono400, 500addresses, signatures, hashes, tallies

All three are SIL Open Font License.

Faces for generated images (next/og)

Given no fonts, next/og's ImageResponse draws with the Geist Regular it bundles, and for any character no font covers it asks fonts.googleapis.com (and cdn.jsdelivr.net for emoji) while the image is drawn: a build that prerenders an image, or a server that draws one, then depends on Google, and offline an emoji fails the whole image. Pass the faces from here instead:

import { ogFonts, ogText } from '@bitbaum/design-tokens/og';

return new ImageResponse(<Card title={ogText(post.title)} />, {
  ...size,
  fonts: await ogFonts(),
});
  • ogFonts() is Geist 400, the face next/og would have used, so the image is

unchanged. ogFonts('inter:400,600') or ogFonts('inter') (every weight shipped) names others; each font is named after its family, and a fontFamily no font has falls back to the first.

  • ogText(text, ...specs) leaves out what none of the faces draws: a word

missing a letter (another script) whole, an emoji or symbol on its own. Use it on text the image does not control, such as a name or a title from a database.

  • For such text, pass Inter after Geist: ogFonts('geist', 'inter:400') and

ogText(text, 'geist', 'inter:400'). Satori draws with Geist and takes what Geist lacks (Greek, Vietnamese, ₿, ✓, many arrows) from Inter, without a fetch; what Geist draws looks as before.

  • The renderer reads neither WOFF2 nor variable fonts, so these are static

instances stored as WOFF in JS modules (og/faces/): any runtime (edge or Node) and any bundler imports them, and a route loads only the faces it names. Shipped: Geist 400, Inter 400 and 600. A face it lacks is added to OG_FACES in scripts/og-fetch-fonts.mjs and released, never fetched.

Faces for Next apps (next/font/local)

next/font/google downloads its faces during next build, so a build fails whenever Google Fonts does not answer. The faces the fleet's apps use ship in fonts/<face>/ instead, each beside its OFL.txt, and fonts/faces.json lists every file with the subset, style, weight range, unicode-range and SHA-256 Google serves it under:

Face (slug)WeightsStylesSubsets
Inter (`inter`)100–900 variablenormallatin, latin-ext, cyrillic, cyrillic-ext, greek, greek-ext, vietnamese
Space Grotesk (`space-grotesk`)300–700 variablenormallatin, latin-ext, vietnamese
IBM Plex Mono (`ibm-plex-mono`)400, 500, 600normallatin, latin-ext, cyrillic, cyrillic-ext, vietnamese
Fraunces (`fraunces`)100–900 variablenormal, italiclatin, latin-ext, vietnamese
Cormorant Garamond (`cormorant-garamond`)300–700 variablenormal, italiclatin, latin-ext, cyrillic, cyrillic-ext, vietnamese
DM Sans (`dm-sans`)100–1000 variablenormallatin, latin-ext
Geist (`geist`)100–900 variablenormallatin, latin-ext, cyrillic, cyrillic-ext, vietnamese
Geist Mono (`geist-mono`)100–900 variablenormallatin, latin-ext, cyrillic, cyrillic-ext, symbols2, vietnamese
JetBrains Mono (`jetbrains-mono`)100–800 variablenormallatin, latin-ext, cyrillic, cyrillic-ext, greek, vietnamese
Montserrat (`montserrat`)100–900 variablenormallatin, latin-ext, cyrillic, cyrillic-ext, vietnamese

next/font takes only literal options in the calling module, and one call carries one unicode-range, so an app gets its font module from a command rather than from a helper:

pnpm exec design-tokens-next-fonts src/app/fonts.ts \
  'sans=inter;variable=--font-inter' \
  'display=space-grotesk;weight=500,700;variable=--font-space-grotesk'
// app/layout.tsx
import { display, sans } from './fonts';
<html className={`${sans.variable} ${display.variable}`}>

Keys after the face: weight= (default: the whole range), style= (default normal), variable=, subsets= (default: all shipped), preload= (default latin; none), display= (default swap), hinting=on (the TrueType-hinted files in fonts/<face>/hinted/, which Google serves a current Chrome on Windows or Linux; the default is the unhinted ones next/font/google downloaded). It writes one localFont call per subset, in Google's order: the same files, weights, styles, ranges and preloads next/font/google produced, formatted with the app's own prettier. The first lines of the file are the command that wrote it; add --check to fail when the file and the package disagree. Two things differ from Google, and neither changes a rendered page: the family is named after the export (Turbopack ignores a font-family declaration in .variable), and the fallback face is measured from the file instead of Google's table.

node scripts/fetch-fonts.mjs (maintainers only, never in a build) is how the files got here: it asks Google exactly what next/font/google asks, with its user agent.

Why a serif

Every AI and infrastructure company competes inside the same dark-canvas grotesque convention, where the winner is whoever has the largest design budget. A high-contrast serif exits that comparison: it reads institutional rather than startup, which is exactly right for a stack whose product is legitimacy — signed votes, verifiable treasuries, an append-only audit trail.


Part of bitbaum — AI-native products on open infrastructure, built in Zürich. Every package here lists the apps that use it: which apps use design-tokens.

Data maps

--map-scale-1 … --map-scale-7 are the one sequential scale for every choropleth (viridis, dark to light; hex, so an SVG fill takes them as is). Luminance rises at every step, so the scale survives colour-vision deficiency and greyscale; test/contrast.test.js keeps it that way. --map-no-data and --map-boundary follow the theme. An area more than one authority claims is hatched, never given a colour of its own: lines of --map-hatch-width every --map-hatch-gap at --map-hatch-angle, in the text colour. Colour is never the only carrier: every map has a list and a legend.

Changelog and releases

Read the repository changelog

This retrospective record was added on 2026-10-27 from the immutable sources linked below. Git commit dates are source dates, not inferred npm publication dates. A GitHub release does not itself establish npm publication. Application adoption and deployment evidence are tracked separately.

1.5.2

  • ogText no longer drops the word an emoji is attached to: U+FE0F (emoji

presentation, in "♻️" or "☕️") is a mark, which 1.5.1 took for a missing letter, so "♻️," lost its comma. A grapheme counts as a letter when it starts with one. Punctuation that followed a dropped emoji joins the word before it ("Kreislaufwirtschaft ♻️, Arbeitsintegration" → "Kreislaufwirtschaft, Arbeitsintegration"); spacing nothing was dropped from is kept.

1.5.1

  • ogText leaves out a word that is missing a letter, whole, instead of the

letters alone: in Geist, which has λ but not the rest of Greek, "Ελλάδα" became "λλ". An emoji or symbol is still left out on its own, and the result is trimmed. README: for text an image does not control, pass Inter after Geist (ogFonts('geist', 'inter:400'), same specs to ogText); satori draws what Geist lacks from Inter (Greek, Vietnamese, ₿, ✓, arrows) and fetches nothing.

1.5.0

  • @bitbaum/design-tokens/og: fonts for generated images (next/og's

ImageResponse: opengraph-image, icon, route handlers). fonts: await ogFonts() hands the renderer Geist 400, the face it falls back to when given none, so an image looks as it did; ogFonts('inter:400,600') names others. ogText(text) drops characters the faces do not cover. Given no fonts, or text a font lacks, next/og fetches from fonts.googleapis.com (and cdn.jsdelivr.net for emoji) at render time. The faces are static WOFF in JS modules (og/faces/), written by scripts/og-fetch-fonts.mjs from Google's static TTFs: Geist 400, Inter 400 and 600.

1.4.0

  • Every face also ships TrueType-hinted, in fonts/<face>/hinted/

(hinted on each entry of fonts/faces.json): what Google serves a current Chrome on Windows or Linux. next/font/google asks as a Mac and gets the unhinted files, which stay the default; a site that downloaded its faces from a browser has the hinted ones, and they draw differently on Windows and Linux. design-tokens-next-fonts takes hinting=on to keep them. OrangeCat's src/fonts were these files, byte for byte.

1.3.1

  • design-tokens-next-fonts: the export (the call that carries .variable

and is measured for the fallback face) is the latin subset whenever latin is declared. With preload=latin,latin-ext,cyrillic 1.3.0 picked cyrillic, whose file has no latin letters, so the fallback was sized at 100% instead of Inter's 107.89%.

1.3.0

  • Faces for apps that used next/font/google, as woff2 with their SIL OFL

licences: Inter (100–900, all seven Google subsets), Space Grotesk, IBM Plex Mono 400/500/600, Fraunces, Cormorant Garamond, DM Sans, Geist, Geist Mono, JetBrains Mono and Montserrat, in fonts/<face>/, listed in fonts/faces.json. The files are the ones Google serves next/font/google.

  • design-tokens-next-fonts writes an app's next/font/local module from

them (README, "Faces for Next apps"). No build has to reach Google.

  • Unchanged: tokens.css, composition.css, the preset, and every existing

file under fonts/.

1.2.0

No authored release notes were found for this tag when this record was made. The links identify the verified source; missing feature history is not invented.

  1. v1.5.0
    Release notes

    What's Changed

    * feat(og): fonts for generated images, so next/og never asks Google by @catomean in https://github.com/bitbaum/design-tokens/pull/52

    Full Changelog: https://github.com/bitbaum/design-tokens/compare/v1.4.0...v1.5.0

  2. v1.4.0
    Release notes

    What's Changed

    * fix(release): publish a version git-release.yml tagged first by @catomean in https://github.com/bitbaum/design-tokens/pull/50 * feat(fonts): ship the hinted faces too, for apps that had them by @catomean in https://github.com/bitbaum/design-tokens/pull/51

    Full Changelog: https://github.com/bitbaum/design-tokens/compare/v1.3.1...v1.4.0

  3. v1.3.1
    Release notes

    What's Changed

    * fix(next-fonts): the latin subset carries the export when latin is declared by @catomean in https://github.com/bitbaum/design-tokens/pull/49

    Full Changelog: https://github.com/bitbaum/design-tokens/compare/v1.3.0...v1.3.1

  4. v1.3.0
    Release notes

    What's Changed

    * chore(deps-dev): bump eslint from 10.12.0 to 10.13.0 in the minor-and-patch group by @dependabot[bot] in https://github.com/bitbaum/design-tokens/pull/38 * docs: record verified package release history by @catomean in https://github.com/bitbaum/design-tokens/pull/40 * chore: add the fleet package template files this repo is missing (git-release.yml, release-notify.yml, release.yml, AGENTS.md, CLAUDE.md) by @catomean in https://github.com/bitbaum/design-tokens/pull/39 * docs(readme): install from npm, the way the registry says by @catomean in https://github.com/bitbaum/design-tokens/pull/41 * docs(agents): the org rules block, generated from bitbaum/fleet by @catomean in https://github.com/bitbaum/design-tokens/pull/42 * chore(deps): dependabot leaves TypeScript 7 alone until typescript-eslint supports it by @catomean in https://github.com/bitbaum/design-tokens/pull/43 * fix(release): respect the package build-output policy by @catomean in https://github.com/bitbaum/design-tokens/pull/44 * docs(agents): the org rules block, generated from bitbaum/fleet by @catomean in https://github.com/bitbaum/design-tokens/pull/45 * chore(deps): clear dev-toolchain audit findings inside existing ranges by @catomean in https://github.com/bitbaum/design-tokens/pull/46 * chore(deps-dev): bump @types/node from 26.6.5 to 26.6.6 in the minor-and-patch group by @dependabot[bot] in https://github.com/bitbaum/design-tokens/pull/47 * feat(fonts): ship the faces fleet apps load from Google, and their next/font/local calls by @catomean in https://github.com/bitbaum/design-tokens/pull/48

    Full Changelog: https://github.com/bitbaum/design-tokens/compare/v1.2.0...v1.3.0

Recent source changes

Repository activity can include unreleased work; it is not a list of deployed changes.

  1. chore(deps-dev): bump the minor-and-patch group with 2 updates (#57)
  2. docs(agents): the org rules block, generated from bitbaum/fleet (#56)
  3. fix(og): ogText keeps the word and punctuation an emoji is attached to (#55)
  4. fix(og): ogText leaves out a word missing a letter, not half of it (#54)
  5. fix(release): publish when git-release tags between the check and the push (#53)
  6. feat(og): fonts for generated images, so next/og never asks Google (#52)
  7. feat(fonts): ship the hinted faces too, for apps that had them (#51)
  8. fix(release): publish a version git-release.yml tagged first (#50)
All releases · All tags

Where it is declared

Linked products declare this package in their manifests. A declaration alone does not verify every feature, version or deployed integration.

Sources and verification

Install commands and declared consumers come from Fleet’s generated registry. Documentation and release notes come from the package repository. The catalogue does not certify production journey coverage.

Documentation from source commit d8fb5fdaa329 · 2026-11-07. Source documentation can include work beyond the registry release.

Package verification runs