Skip to content
HomePagesHomePages template kit

Tokens in `TokenTheme`

The `TokenTheme` a template's `theme.ts` exports — its token records, the document block, and the `theme.css` the CLI compiles from it.

Your template owns its design system end to end — the token names included. You declare the tokens your design actually has in a TokenTheme, and template-kit theme compiles that one declaration into the Tailwind utilities that expose them. Declare a colour named lagoon and bg-lagoon exists.

A template’s theme.ts exports a TokenTheme: six open records of tokens, plus the document defaults that dress the page. theme.ts is the file you write; run template-kit theme to compile it into the theme.css your Tailwind entry imports (see Generating theme.css below).

import type { TokenTheme } from "@homepages/template-kit";
export const theme: TokenTheme = {
colors: { ink: "#111418", page: "#ffffff", "surface-alt": "#f4f4f5", lagoon: "#1c6e7d" },
type: { base: "1rem", "2xl": "1.5rem", display: "clamp(2.5rem, 6vw, 4.5rem)" },
radii: { md: "6px", pill: "9999px" },
shadows: { card: "0 1px 2px rgb(0 0 0 / 0.06)" },
layout: { "container-max": "1280px", "header-height": "72px" },
// A font token is a plain stack, or an ENTRY that also loads the face.
// See "Loading a web font" below — a rendered template must ship its own files.
fonts: {
mono: "ui-monospace, monospace", // a system face: nothing to load
sans: {
family: "Inter", // written once — the stack derives from it
fallback: "sans-serif", // expands to the full system tail
files: [{ src: "./fonts/inter.woff2", weight: "100 900", subset: "latin" }],
},
},
// Optional. Tailwind's sm/md/lg/xl/2xl are already there; add what your design
// swaps at beyond them. Each name becomes a variant prefix — `phone:flex`.
breakpoints: { phone: "390px", wide: "1440px" },
// Which of YOUR tokens dress the document.
document: { font: "sans", color: "ink", background: "page", fontSize: "base" },
};

Nothing is required to exist. Tailwind’s own defaults survive underneath your tokens — text-2xl, rounded-lg, red-500 all still work — so a sparse theme is a working theme. (Breakpoints are yours too — the kit ships none and switches none off. See breakpoints are yours.)

Why a design is declared this way — as named tokens a section spends, rather than as literal values — is the theme model. Each record maps to one Tailwind theme namespace. A token’s key is emitted verbatim into both the custom property and the utility — no camelCase-to-kebab transform, so what you type is what you get.

Record Custom property Tailwind utilities Example
colors --tr-color-<name> bg-, text-, border-, ring-, fill-, stroke-, … lagoonbg-lagoon
fonts --tr-font-<name> font-<name> displayfont-display
type --tr-text-<name> text-<name> 2xltext-2xl
radii --tr-radius-<name> rounded-<name> pillrounded-pill
shadows --tr-shadow-<name> shadow-<name> cardshadow-card
layout --tr-<name> (none) header-heightvar(--tr-header-height)
breakpoints --breakpoint-<name> a <name>: variant prefix phonephone:flex

layout has no Tailwind utility namespace, so its tokens get a --tr-* value and no utility — reference them with var() from your own CSS.

breakpoints is optional, and the one group that does not go through --tr-*: a media query needs a literal length, so its values are emitted directly. Tailwind’s own sm/md/lg/xl/2xl are already available and the kit adds none, so declare only what your design swaps at beyond them. A name that collides with a Tailwind state variant (hover, dark, print, …) is rejected; redefining one of Tailwind’s own breakpoints is fine.

Kebab-case: [a-z0-9] groups separated by single hyphens (ink, ink-soft, bp2). TokenNameSchema is exported if you want to validate names yourself.

One token, one utility. Two utility names cannot share a token — if you want both bg-brand and bg-primary, declare both.

Required. It names which of your own tokens dress the page — the body font-family, colour, background, and font-size. font and fontSize resolve against fonts and type; color and background resolve against colors.

Every reference is validated against your declared tokens, in its own group: a typo, or a colour name where a font belongs, fails at parse rather than rendering an unthemed page in silence.

A font token can be a plain CSS stack, which loads nothing:

fonts: { mono: "ui-monospace, SFMono-Regular, monospace" },

Or an entry, which names the family once and lets the kit write the rest — the @font-face rules and the stack behind font-<name>. You never type the family twice, so the name the browser loads and the name your CSS asks for cannot disagree. Two ways to fill it, and the template’s output class decides which:

Ship the files. Put them in the template’s own fonts/ folder, licence alongside, and give one files element per file:

fonts: {
sans: {
family: "Inter",
fallback: "sans-serif",
files: [
{ src: "./fonts/inter-latin.woff2", weight: "100 900", subset: "latin" },
{ src: "./fonts/inter-latin-ext.woff2", weight: "100 900", subset: "latin-ext" },
],
},
},

Each element compiles to exactly one @font-face. src resolves against theme.ts’s own directory and is rewritten to a content-hashed asset path, and the files are published with the template, so the same bytes serve the preview and the deliverable.

Point at a hosted stylesheet with href instead, and the visitor’s browser fetches it. Several tokens may carry the same href — it is emitted once, so four families from one stylesheet stay one request.

fonts: { sans: { family: "Inter", fallback: "sans-serif", href: "https://…" } },
  • fallback is a keyword — sans-serif, serif or monospace — that expands to the full system tail, so family: "Inter", fallback: "sans-serif" becomes "Inter", ui-sans-serif, system-ui, -apple-system, …, sans-serif. Want a different tail (fall back to another webfont first, say)? That is a plain stack, not an entry.
  • subset names a standard subset — latin, latin-ext, cyrillic, cyrillic-ext, greek, greek-ext, vietnamese — and the kit supplies its unicode-range. That range is what keeps a latin-ext file off a page whose copy is pure ASCII. Omit it when one file covers everything; an unknown name is an error, not a silently dropped range.
  • format() comes from the file extension, and font-display: swap is always set.
  • weight and style are yours, per file, because they are the two things the kit cannot see. One variable font file covers a whole range: give it weight: "100 900" and the browser interpolates every weight your design spends, instead of synthesising a fake bold from a single static cut. Static cuts are ordinary entries too — one element each, with their own weight and style: "italic".

Prefer files when you have the bytes. Both entry shapes work in every output class — a rendered deliverable’s capture fetches a hosted stylesheet like any other ref, and a fetch that fails fails the render naming the URL rather than falling back to a system face. Shipping the files still buys what hosting cannot: the same bytes serve the preview and the deliverable with no third party in the render path, and the artifact stays reproducible — the same template renders the same characters a year later.

TokenThemeSchema validates the object at runtime, and it is .strict() — an unknown top-level key is an error rather than a silently-dead set of tokens.

template-kit check grades the same file for every template it checks, whatever the template’s output class: the file has to be there, it has to export the reserved binding theme, and that export has to parse. See theme-invalid.

template-kit theme loads templates/<key>/theme.ts, validates its theme export against TokenThemeSchema, and writes templates/<key>/theme.css — the file your Tailwind entry imports.

Its arguments, flags and exit codes are on the theme reference page.

theme.css is generated — it opens with a header saying so, and it is not meant to be hand-edited. Re-run template-kit theme after changing theme.ts and commit the regenerated file alongside it.

A missing theme.ts, a theme.ts that doesn’t export theme, or a theme that fails TokenThemeSchema all fail the command with a message naming the file and the problem — nothing is written in that case.

In order, the file it writes contains:

  1. @theme inline { --color-<name>: var(--tr-color-<name>); … } — the alias layer, which is what brings your token utilities into existence;
  2. @theme { --breakpoint-<name>: <value>; … } — your breakpoints, if you declared any;
  3. :root { --tr-color-<name>: <value>; … } — the values behind those aliases;
  4. body { … } — your document defaults, written as var() indirections so a runtime :root override still cascades to the page.

Your font faces are not in this file. They are compiled separately and prepended to the finished bundle, because an @import nested inside a partial the Tailwind entry imports is dropped — silently, leaving a page whose fonts never load.

Deterministic: keys are sorted, so the same theme always content-hashes identically.

  • The theme model — why a design is declared as named tokens rather than literal values.
  • The CSS pipeline — where the compiled theme.css is imported, and the two CSS entries around it.
  • template-kit theme — the command’s arguments, flags and exit codes.
  • no-hex — why a section spends tokens instead of naming colours.