The Theme contract
The 21 core roles every Theme sets, the derived roles that default through var(), the invariant roles, the plate remap, and what compileTheme refuses.
startupbuilder.dev defines the Theme: "A Theme is the design tokens of a Brand, its color, type, spacing, radius, and motion, that every Component reads when it renders." mdxui adds five policies and compiles the tokens to CSS custom properties.
Three kinds of role
A role names a use, not a look: ink-lede is the color a lede is set in, whatever that color is. Every Section type reads roles and never a literal color, typeface or size.
| Kind | A Theme | Example |
|---|---|---|
| Core | Sets every one, in light or invariant | paper, ink, accent |
| Derived | May override any; each defaults to a core role through var() | ink-lede defaults to the secondary ink |
| Invariant | Declares once and never redefines for dark | certificate's plate family |
From @mdxui/themes@7.0.0-alpha.0: 21 core roles, which every Theme sets, and 325 derived roles, each with a default.
paper, panel, ink, ink-soft, rule, rule-soft, accent, accent-ink, pass, fail, warn, plate, plate-ink, plate-rule, sans, mono, text, lh, shell-max, shell-inset, shell-d-section
Core roles
Each square is one role under one Theme, as resolveTokens returns it. Hover for the value; click to copy the custom property. Dark shows the palette a Theme emits, so the three light-only Themes keep their light values.
| Role | certificate | vaultline | quaypoint | callboard | directory |
|---|---|---|---|---|---|
paper | |||||
panel | |||||
ink | |||||
ink-soft | |||||
rule | |||||
rule-soft | |||||
accent | |||||
accent-ink | |||||
pass | |||||
fail | |||||
warn | |||||
plate | |||||
plate-ink | |||||
plate-rule |
| Role | certificate | vaultline | quaypoint | callboard | directory |
|---|---|---|---|---|---|
sans | IBM Plex Sans | Public Sans | Inter | Instrument Sans | Inter |
mono | IBM Plex Mono | IBM Plex Mono | JetBrains Mono | DM Mono | JetBrains Mono |
text | 15px | 16px | 16px | 16px | 14px |
lh | 1.6 | 1.5 | 1.5 | 1.55 | 1.5 |
shell-max | 1320px | 1180px | 1200px | 1200px | 1200px |
shell-inset | clamp(1rem,3.5vw,2.5rem) | clamp(20px,4vw,32px) | clamp(20px,4vw,36px) | clamp(20px,4vw,36px) | clamp(16px,4vw,32px) |
shell-d-section | clamp(2.25rem,5vw,3.5rem) | clamp(70px,9vw,104px) | clamp(72px,9vw,104px) | clamp(72px,9vw,104px) | 24px |
Derived roles
A Theme that sets only the core roles renders: every derived role falls back through var() to a core role. A richer Theme overrides the families its design needs. The bars count how many roles of each family a Theme sets itself.
| Family | Roles | certificate | vaultline | quaypoint | callboard | directory |
|---|---|---|---|---|---|---|
| All 49 families | 325 | 29 | 254 | 122 | 126 | 126 |
fg-*fg-gap: 1.9rem | 27 | 0 | 0 | 0 | ||
tier-*tier-gap: 1.9rem | 27 | 0 | ||||
prose-*prose-ink: var(--ink-soft) | 27 | 0 | 0 | 0 | 0 | |
footer-*footer-brand-size: var(--text) | 27 | |||||
nav-*nav-height: 58px | 21 | |||||
plate-*plate-panel: var(--plate) | 19 | |||||
btn-*btn-face: var(--mono) | 18 | |||||
app-*app-nav-active-bg: color-mix(in srgb, var(--accent) 10%, transparent) | 16 | 0 | 0 | 0 | 0 | 0 |
hero-*hero-h1-gap: 1.1rem | 14 | 0 | 0 | 0 | ||
term-*term-bg: var(--plate-well) | 12 | 0 | ||||
shell-*shell-d-compact: var(--shell-d-section) | 10 | |||||
sechead-*sechead-max: none | 9 | 0 |
The other 37 families
| Family | Roles | certificate | vaultline | quaypoint | callboard | directory |
|---|---|---|---|---|---|---|
ink-*ink-lede: var(--ink-soft) | 7 | 0 | ||||
h2-*h2-size: clamp(1.4rem,3.4vw,2.15rem) | 7 | 0 | ||||
article-*article-rule: var(--rule-soft) | 6 | 0 | 0 | 0 | ||
accent-*accent-hover: var(--accent) | 5 | 0 | ||||
radius-*radius-control: 0 | 5 | 0 | ||||
code-*code-ink: var(--plate-ink) | 4 | 0 | ||||
brand-*brand-weight: 700 | 4 | 0 | ||||
post-*post-title-size: clamp(23px,3.2vw,30px) | 4 | 0 | 0 | 0 | 0 | |
share-*share-label-ink: var(--ink-muted) | 4 | 0 | 0 | 0 | 0 | |
shadow-*shadow-card: none | 4 | 0 | ||||
h1-*h1-size: clamp(2.2rem,7vw,3.4rem) | 3 | 0 | 0 | |||
title-*title-size: var(--h1-size) | 3 | 0 | 0 | 0 | 0 | |
eyebrow-*eyebrow-size: var(--mono-xs) | 3 | |||||
listing-*listing-title-size: clamp(34px,6vw,52px) | 3 | 0 | 0 | 0 | 0 | |
field-*field-a: color-mix(in oklch, var(--accent), white 18%) | 3 | 0 | 0 | |||
panel-*panel-inset: var(--panel) | 2 | 0 | ||||
glass-*glass: var(--paper) | 2 | 0 | ||||
cta-*cta: var(--accent) | 2 | 0 | ||||
display-*display: var(--sans) | 2 | 0 | ||||
text-*text-sm: calc(var(--text) * .875) | 2 | 0 | 0 | 0 | 0 | |
mono-*mono-sm: var(--text-sm) | 2 | 0 | 0 | 0 | 0 | |
link-*link: currentColor | 2 | 0 | ||||
btitle-*btitle-size: var(--h2-size) | 2 | 0 | 0 | 0 | 0 | |
compare-*compare-no: var(--ink-faint) | 2 | 0 | 0 | 0 | 0 | |
fsplit-*fsplit-copy: 400px | 2 | 0 | 0 | 0 | 0 | 0 |
dur-*dur-fast: 120ms | 2 | 0 | 0 | 0 | 0 | |
tint-*tint: var(--paper) | 1 | 0 | ||||
track-*track: var(--rule-soft) | 1 | |||||
rule-*rule-faint: var(--rule-soft) | 1 | 0 | ||||
ghost-*ghost-border: var(--rule) | 1 | 0 | ||||
figures-*figures: normal | 1 | 0 | 0 | 0 | 0 | |
heading-*heading-wrap: balance | 1 | 0 | 0 | 0 | 0 | |
meta-*meta-accent: var(--eyebrow-color) | 1 | 0 | 0 | 0 | 0 | |
hatch-*hatch: color-mix(in srgb, var(--ink) 16%, transparent) | 1 | 0 | 0 | 0 | 0 | |
scrim-*scrim: rgba(0, 0, 0, 0.58) | 1 | 0 | 0 | 0 | 0 | |
ease-*ease-out: cubic-bezier(0.16, 1, 0.3, 1) | 1 | 0 | 0 | 0 | 0 | |
z-*z-nav: 50 | 1 | 0 | 0 | 0 |
@mdxui/themes exports lists every derived role with its default.
Invariant roles
An invariant role is declared once and never redefined for dark. compileTheme refuses a Theme that names one in dark. certificate keeps its plate and its scrim invariant, so a code window is the same dark plate in both schemes.
plateplate-inkplate-ink-softplate-ruleplate-accentplate-passscrimand 17 type, size and motion roles: ease-out, dur-fast, dur-base, mono, sans, text, lh, text-sm, text-xs, mono-xs, fg-label-size, eyebrow-size, btn-size, hero-fine-size, nav-link-size, footer-brand-size, footer-note-sizeterm-bgterm-row-bgterm-inkterm-ink-softterm-ink-mutedterm-rule-softterm-rule-faintterm-accentterm-passterm-control-inkterm-command-inkterm-hovercode-inkcode-commentcode-stringcode-keywordterm-bgterm-row-bgterm-inkterm-ink-softterm-ink-mutedterm-rule-softterm-rule-faintterm-accentterm-passterm-control-inkterm-command-inkterm-hovercode-inkcode-commentcode-stringcode-keywordterm-bgterm-row-bgterm-inkterm-ink-softterm-ink-mutedterm-rule-softterm-rule-faintterm-accentterm-passterm-control-inkterm-command-inkterm-hovercode-inkcode-commentcode-stringcode-keywordOn a plate
A plate section re-tokens its subtree: a Section type on a plate reads --ink and gets the plate's ink. PLATE_REMAP names each pair, so one Section type renders on paper and on a plate with no second set of styles.
inkink-softink-ledeink-bodyink-noteink-mutedink-quietink-faintink-ghostrulerule-softrule-faintpanelpanel-insetghost-borderaccentaccent-mutedpassink → plate-inkink-soft → plate-ink-softink-lede → plate-ink-ledeink-body → plate-ink-bodyink-note → plate-ink-faintink-muted → plate-ink-mutedink-quiet → plate-ink-faintink-faint → plate-ink-faintink-ghost → plate-ink-ghostrule → plate-rulerule-soft → plate-rule-softrule-faint → plate-rule-faintpanel → platepanel-inset → plate-panelghost-border → plate-ghost-borderaccent → plate-accentaccent-muted → plate-accent-softpass → plate-pass18 remaps in PLATE_REMAP. A Section type on a plate reads --ink and gets the plate’s ink; the compiler declares every chained role again on the plate.
The Theme object
interface Theme {
name: string;
light: Record<string, string>; // the full light palette, every core role included
dark?: Record<string, string>; // only what changes for dark
invariant?: Record<string, string>; // declared once, never redefined for dark
dividers: DividerPolicy;
markers: boolean;
featured: 'rule' | 'plate';
icons: 'inline' | 'image';
scheme?: 'auto' | 'light';
}Compiling
compileTheme(theme) checks a Theme against the contract and returns its custom properties for :root. It refuses a Theme missing a core role and names every missing one. For a Theme that sets only paper:
theme "harbor" is missing core roles: panel, ink, ink-soft, rule, rule-soft, accent, accent-ink, pass, fail, warn, plate, plate-ink, plate-rule, sans, mono, text, lh, shell-max, shell-inset, shell-d-sectionresolveTokens(theme) returns the light palette with every var() chain followed to its value, for wherever a role is needed as a literal. The playground runs both on every edit.
Prior definitions
| Prior | Definition | Disposition |
|---|---|---|
| startupbuilder.dev Theme | The design tokens of a Brand, its color, type, spacing, radius, and motion | Adopted word for word. The 5 policies are the mdxui addition. Its css property, extra CSS after the tokens, has no counterpart: no page carries its own CSS. |
@mdxui/themes contract | Role and value pairs compiled to CSS custom properties | Kept as the rendering view of the same Noun |
Legacy mdxui theme prop | light, dark or system on a Site wrapper | Superseded by the scheme policy and the dark palette |