Theme and components
Cookbook resolves Glaze colors and Tasty styles during the build, then ships static CSS. Start with a brand color; add semantic palette roles, typography, and component overrides only where the default needs to change. The configuration recipes show complete, build-checked starting points. Read the customization rules before writing new styles.
| To change | Start here |
|---|---|
| Brand, color roles, and design tokens | This page |
| Font files, families, and typography | Fonts and typography |
| Built-in UI surfaces and named parts | Component styles |
| Your own Astro or MDX components | Custom components |
| Complete configuration fields | Configuration reference |
Glaze derives light, dark, and high-contrast values from the brand and palette inputs. Tasty applies those values through semantic tokens. See the Glaze reference for color declarations and the Tasty style DSL for state maps, tokens, units, and sub-elements.
The default brand is a calm blue with 68% OKHSL saturation. Set a brand color
with Glaze’s from declaration:
import { defineDocsConfig } from "@tenphi/cookbook/config";
export default defineDocsConfig({
theme: { brand: { from: "#2f5bff" } },
});Add a contrast target when the brand appears as text:
theme: {
brand: {
from: "#2f5bff",
contrast: { apca: [45, 60] }
}
}The brand supplies the hue and saturation seed. Cookbook derives separate text, filled-control, and focus roles from it. Small accent text and filled controls use stronger default APCA floors (75 normally, 90 in high contrast), and diagnostics also enforce WCAG 4.5:1 / 7:1 on their actual backgrounds. Higher authored brand targets are retained. Glaze adjusts tones only as far as these floors require; dark and high-contrast schemes resolve independently.
Cookbook rejects a normal APCA target below 45 unless
unsafeContrast: true is present. That escape hatch is intentionally visible
in configuration reviews. Literal color shorthand and structured
hue/saturation/tone input also remain supported for the brand.
brand controls accent text, fills, and focus. The optional palette roles
control the reading surface, text, and callout colors. Declare color
relationships with Glaze’s tone, base, and contrast properties:
theme: {
brand: { from: "#2f5bff" },
palette: {
surface: { tone: 98, saturation: 0.05 },
text: {
base: "surface",
tone: 0,
saturation: 0,
contrast: { wcag: [7, 10] }
},
heading: {
base: "surface",
tone: [4, 0],
saturation: 0,
contrast: { wcag: [7, 10] }
},
"text-soft": {
base: "surface",
tone: [25, 10],
saturation: 0.05,
contrast: { wcag: [4.5, 7] }
}
},
contrastLevel: "auto"
}Glaze resolves each declaration for light, dark, normal, and high-contrast
modes. In a tone or contrast pair, the first value applies to normal mode;
the second applies to high contrast. Use absolute tones for reading text:
text starts at tone 0 and heading at tone 4, giving headings only a slight
reduction in contrast. Glaze applies its tone boundaries in normal mode, so
body text is not pure black, then inverts the tones for dark mode. High-contrast
mode uses the full range. text-soft is reserved for secondary text.
heading follows an explicitly configured text declaration unless it has
its own declaration, preserving existing custom palettes.
Contrast requirements are minimum safeguards: Glaze preserves the authored
tone when it already meets the floor. A small relative tone step from the
surface would instead leave the solver to produce text at that minimum.
Palette declarations inherit the brand hue and saturation:
saturation is a 0–1 factor of that seed, and tone is 0–100. They can set
an absolute tone or a tone relative to base. Use from on a palette role
when it needs its own color seed; otherwise relative declarations keep its
relationship to the brand and surrounding surface as the scheme or contrast
changes. info, success, warning, and danger also accept Glaze
declarations and expose border, -text, and -surface semantic tokens.
Cookbook keeps literal surface seeds desaturated in the dark scheme so a nearly
white tint does not become vivid dark chrome when its tone is inverted.
Components consume semantic colors consistently: surface, header, surface-2,
surface-3, text, heading, text-soft, sidebar-text, border, border-strong, accent-text,
accent-surface, accent-surface-text, logo-surface, logo-mark, and focus.
Tasty components can use these as #surface, #text, #border, and so on; the Astro shell consumes the
same resolved values. Glaze also generates hover and pressed states, subtle
accent fills, overlays, shadows, and the orange, green, blue, purple, and red
roles used by Cookbook content components. No browser color mixes or
upstream fallback palette values participate in the rendered theme. Surface
elevation uses Glaze’s contrast-uniform tone axis: surface-2 advances two tone
steps and surface-3 advances four from the base surface, with proportionally
wider steps in high-contrast mode. Their saturation also steps down to 75% and
65% of the authored surface seed so tinted surfaces remain restrained as they
move farther from the base tone.
Borders stay intentionally quiet: Glaze derives their hue from brand but
uses only one quarter of the brand saturation. Normal and high-contrast modes
change border tone, not that restrained saturation relationship.
Every theme.palette entry participates in one Glaze color graph. Use lowercase
names with hyphens; a role named review-panel becomes the Tasty token
#review-panel. Declaration order does not matter. References can target custom
colors or built-in roles, including header, border, overlay, and syntax colors.
theme: {
brand: { from: "#315efb" },
palette: {
"review-ink": {
base: "review-panel",
tone: [4, 0],
saturation: 0.05,
contrast: { wcag: [7, 10] },
},
"review-panel": { base: "surface", tone: "-2", saturation: 0.05 },
},
styles: {
Callout: { fill: "#review-panel", color: "#review-ink" },
},
},Relative and absolute declarations inherit the brand’s hue and saturation.
Use saturation factors from 0 (neutral) to 1 (full seed saturation), and
hue to choose a different hue. All roles resolve for light, dark, and both
high-contrast modes. Configuration diagnostics and rendering use the same graph.
Missing references and cycles fail with the color name; layered configurations
resolve references after merging.
Palette names use lowercase letters, digits, and hyphens, such as text-soft.
The names current, constructor, and prototype are reserved.
Declaring a built-in name intentionally overrides that role; use a project
prefix for additional roles to avoid future naming collisions.
When a status role such as info uses a mix or shadow definition, its derived
info-text defaults to neutral, contrast-corrected text. Override info-text
explicitly for a colored label; the mix/shadow dependency graph stays intact.
A shadow status also uses a neutral derived surface, because Glaze does not allow
a shadow to be a mix target. Override the surface separately when needed.
theme.glaze accepts lightTone, darkTone, darkDesaturation, autoFlip,
pastel, inferRole, and shadowTuning. Tone windows accept [lo, hi], false
for the full range, or the advanced { lo, hi, eps } form. Continue to use
theme.contrastLevel for manual contrast interpolation. Cookbook always emits
all four appearance variants.
theme: {
glaze: { lightTone: [10, 100], darkTone: [15, 95] },
palette: {
"accent-surface-subtle": {
type: "mix", base: "surface", target: "accent-surface", value: [12, 20],
},
shadow: {
type: "shadow", bg: "surface", fg: "text", intensity: [12, 24],
tuning: { alphaMax: 0.3 },
},
},
},Palette roles accept Glaze’s regular, mix, and shadow declarations. Mix and
shadow references use the same graph as other roles. Native Glaze restrictions
still apply, including the requirement for shadow backgrounds and foregrounds
to reference non-shadow colors. shadowTuning exposes saturationFactor,
maxSaturation, lightnessFactor, lightnessBounds, minGapTarget, alphaMax,
and bgHueBlend; a role’s own tuning takes precedence.
Every generated role below can be overridden in theme.palette. Names map
directly to Tasty #name tokens. COOKBOOK_PALETTE_NAMES, exported from
@tenphi/docs, lists the same roles for tooling.
| Area | Roles |
|---|---|
| Surfaces | surface, header, overlay, surface-2, surface-3, surface-2-hover, surface-2-pressed, surface-3-hover, surface-3-pressed |
| Reading | text, heading, text-soft, text-muted, sidebar-text |
| Brand and focus | accent-text, accent-surface, accent-surface-text, accent-surface-subtle, accent-surface-2-subtle, logo-surface, logo-mark, focus |
| Borders and effects | border, border-strong, shadow, clear |
| Status | info, success, warning, danger, each with -text and -surface variants |
| Additional hues | orange, green, blue, purple, red, each with -text and -surface variants |
| Syntax | syntax-bg, syntax-text, syntax-comment, syntax-punctuation, syntax-keyword, syntax-string, syntax-token, syntax-property, syntax-number, syntax-function, syntax-value, syntax-operator |
Cookbook checks text, headings, muted reading text, brand links, filled-control
labels, status text on its tinted surface, focus rings on each elevated
surface, and every palette declaration with a contrast floor. The same Glaze
resolution powers rendering and cookbook doctor, in light, dark, and both
high-contrast modes. Manual theme.contrastLevel targets interpolate with
Glaze’s contrast level.
resolveDocsTheme(theme).contrastChecks lists each foreground/background pair,
mode, metric, target, measured value, and pass result. APCA uses display-channel
luminance; WCAG ratios use the sRGB transfer function. Transparent foregrounds
are composited onto their background before measuring. These checks cover
Cookbook’s semantic pairs; test actual pages after changing component styles,
backgrounds, typography, or layout.
A DOCS_SEMANTIC_CONTRAST_UNMET or DOCS_BRAND_CONTRAST_UNMET error names the
pair, mode, and required target. Adjust that pair’s theme.palette declarations:
use an absolute tone for reading text, reduce saturation when needed, and keep
autoFlip enabled when the solver needs to cross its base. Cookbook’s default
fixed brand fill is anchored to a light label and darkens when needed to meet
contrast, including for orange, yellow, and pale brands. A custom fixed
middle-tone fill can make its label’s contrast impossible.
Focus uses a WCAG 3:1 floor (4.5:1 in high contrast) against the surface ramp.
Keep contrast requirements when adjusting colors. Extreme custom tone windows, fixed colors, or opacity can prevent a requested floor from being met. A palette check does not replace keyboard, screen-reader, and rendered-page testing.
Token names follow Tasty’s
token and unit syntax: $name
becomes the CSS custom property --name. Configure tokens with $name keys.
theme: {
tokens: {
"$gap": "0.5rem",
"$radius": "8px",
"$card-radius": "16px",
"$border-width": "1px",
"$outline-width": "2px",
"$outline-offset": "2px",
"$control-height": "2.5rem",
"$layout-width": "87.5rem",
"$content-width": "58rem",
"$sidebar-width": "17.5rem"
}
}layout-width caps and centers the complete documentation shell, while
content-width limits the reading column inside it. The column stays centered
as the viewport narrows: horizontal padding shrinks to 1.5rem before the column
itself shrinks. Mobile layouts use 1rem padding. radius is the control
and navigation radius; card-radius is the larger surface radius. Keeping
those roles separate makes a sharp control theme or a soft card theme possible
without one-off component overrides.
Register custom units and flat recipes in theme; Cookbook configures Tasty
before evaluating your components:
theme: {
brand: { from: "#315efb" },
palette: {
"review-panel": { base: "surface", tone: "-2", saturation: 0.05 },
"review-ink": { base: "review-panel", tone: 0, contrast: { wcag: [7, 10] } },
},
units: { rh: "6px" },
recipes: {
"review-panel": {
fill: "#review-panel",
color: "#review-ink",
padding: "2rh",
radius: "3px",
},
},
customStyles: { DemoBadge: { Label: { color: "#review-ink" } } },
},import { defineComponent } from "@tenphi/cookbook/styling";
export const DemoBadge = defineComponent("DemoBadge", {
as: "aside",
elements: { Label: "strong" },
styles: {
display: "block",
recipe: "review-panel",
Label: { preset: "h4" },
},
});Use <DemoBadge><DemoBadge.Label>Review ready</DemoBadge.Label></DemoBadge> in
MDX. Keep these styled components server-rendered: their CSS is extracted at
build time. Add behavior with a small client script targeting the rendered
markup. This recipe produces 12px padding. Units merge by name, with the later
configuration winning. Recipes merge using Tasty mergeStyles, including state
maps. Recipes are flat: define named sub-elements on the owning component, and
compose recipes with recipe: "base elevated" rather than referencing a recipe
inside another recipe. none is a reserved recipe name. Cookbook’s built-in
units are x, r, cr, and bw; overriding one changes its meaning globally.
Keep editor and linter validation synchronized with your configuration:
// tasty.config.ts
import { createValidationConfig } from "@tenphi/cookbook/eslint-plugin";
import docs from "./docs.config";
export default createValidationConfig(docs.theme);Cookbook supplies semantic body, heading, h1–h6, navigation, small,
and code presets. Change their families, sizes, and weights through
theme.fonts and theme.presets. See Fonts and typography
for local and Google font examples, cache behavior, and preset details.
Use theme.fonts to load Google families by name or local font files. The
font guide explains both
paths and how to select weights and styles.
Use a partial Tasty style object at theme.styles.<ComponentName> to change a
built-in surface. Cookbook merges it with the complete base style object before
CSS extraction. The component styles reference gives
examples and the complete list of named sub-elements for every surface.
Use defineComponent() for a configurable component root and
resolveComponentStyles() for an owned global style tree. Keep styling on the
server so the browser receives extracted CSS. The
custom component guide covers both patterns.
For ordinary branding, set site.logo once for the header and mobile drawer.
Use the logo guide for image
variants, accessibility, and advanced title markup.
Adopt Cookbook’s Tasty validation preset and register your additional names. See Linting custom styles for ESLint and oxlint examples.
The Appearance menu stores each reader’s color scheme and contrast in one
localStorage entry. Its key and format are a stable contract, so another app
on the same origin can share the preference with the documentation. A typical
example is a product site that serves Cookbook under /docs/.
// localStorage["cookbook-appearance"], stored as JSON
type CookbookAppearance = {
theme?: "light" | "dark" | "system";
contrast?: "normal" | "more" | "system";
};A missing or unrecognized field means "system", which follows the reader’s
operating system. Keep any other fields when you write the object:
const key = "cookbook-appearance";
const appearance = JSON.parse(localStorage.getItem(key) ?? "{}") ?? {};
localStorage.setItem(key, JSON.stringify({ ...appearance, theme: "dark" }));Cookbook applies the stored choice before the first paint, as data-theme and
data-contrast on <html>. Open pages follow changes made in other tabs through
the storage event. Your app can map the same values onto its own attributes.
Browsers keep localStorage per origin: the scheme, host, and port must all
match, so a docs subdomain does not share the preference.
The page content remains readable without client JavaScript. Search, menus, copy controls, and appearance persistence enhance the static HTML. See Component styles for the boundary and responsive UI behavior.