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.)
Namespaces and utilities
Section titled “Namespaces and utilities”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-, … |
lagoon → bg-lagoon |
fonts |
--tr-font-<name> |
font-<name> |
display → font-display |
type |
--tr-text-<name> |
text-<name> |
2xl → text-2xl |
radii |
--tr-radius-<name> |
rounded-<name> |
pill → rounded-pill |
shadows |
--tr-shadow-<name> |
shadow-<name> |
card → shadow-card |
layout |
--tr-<name> |
(none) | header-height → var(--tr-header-height) |
breakpoints |
--breakpoint-<name> |
a <name>: variant prefix |
phone → phone: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.
Token names
Section titled “Token names”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.
The document block
Section titled “The document block”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.
Loading a web font
Section titled “Loading a web font”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://…" } },What the kit fills in
Section titled “What the kit fills in”fallbackis a keyword —sans-serif,seriformonospace— that expands to the full system tail, sofamily: "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.subsetnames a standard subset —latin,latin-ext,cyrillic,cyrillic-ext,greek,greek-ext,vietnamese— and the kit supplies itsunicode-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, andfont-display: swapis always set.weightandstyleare yours, per file, because they are the two things the kit cannot see. One variable font file covers a whole range: give itweight: "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 ownweightandstyle: "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.
Validation
Section titled “Validation”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.
Generating theme.css
Section titled “Generating theme.css”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:
@theme inline { --color-<name>: var(--tr-color-<name>); … }— the alias layer, which is what brings your token utilities into existence;@theme { --breakpoint-<name>: <value>; … }— your breakpoints, if you declared any;:root { --tr-color-<name>: <value>; … }— the values behind those aliases;body { … }— yourdocumentdefaults, written asvar()indirections so a runtime:rootoverride 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.
See also
Section titled “See also”- The theme model — why a design is declared as named tokens rather than literal values.
- The CSS pipeline — where the compiled
theme.cssis 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.