This chapter explains how to make AI-generated and editor-assembled pages "follow the rules but never look identical." The design system is the constraint layer between content and pixels: a finite vocabulary of tokens, primitives, and recipes that an LLM (or a human editor) can recombine freely without ever producing something off-brand or broken. We cover the four-layer guardrail stack — design tokens, layout primitives, component recipes, and editorial templates with bounded degrees of freedom — and the 2025–2026 implementation tooling (the now-stable W3C Design Tokens Format Module 2025.10, Style Dictionary, Tailwind CSS v4 @theme, CVA / Tailwind Variants / Panda slot recipes, and shadcn-style component registries). The thesis: constrain the inputs, not the output — give the generator a closed set of safe choices and let variety emerge from combination.
An AI-native CMS has two failure modes pulling in opposite directions. If every page is rendered from one rigid template, the site looks robotic and repetitive — the "AI slop" homogeneity readers now recognize on sight. If the generator (or editor) can emit arbitrary HTML/CSS, you get inconsistent spacing, broken responsiveness, inaccessible color contrast, and brand drift on the first page nobody reviewed.
The resolution is the same principle that lets natural language be infinitely expressive with a finite alphabet: constrained composition. As the Every Layout authors put it, a primitive "is something without its own meaning on its own but can be used in composition to make something meaningful" — like a word in a sentence (every-layout.dev/rudiments/composition). You do not give the generator a blank canvas; you give it Lego bricks with studs that only fit certain ways. The number of valid pages is astronomically large; the number of invalid pages is zero by construction.
This is why the design system functions as guardrails, not as a style guide. A style guide is documentation a human is supposed to read and obey. Guardrails are code: the off-brand choice is literally not expressible. For an LLM author — which will confidently produce whatever it can — that distinction is everything. The model can only pick from tokens that exist, slot into components that exist, and arrange them inside templates that constrain order and density.
| Layer | What it constrains | Primary artifact | Who/what consumes it |
|---|---|---|---|
| 1. Design tokens | Atomic values: color, spacing, type scale, radius, shadow, motion | tokens.json (DTCG 2025.10), CSS variables | Tailwind @theme, Style Dictionary outputs, runtime CSS |
| 2. Layout primitives | Spatial relationships: stack, cluster, grid, sidebar, switcher | <Stack>, <Grid>, <Cover> components | Recipes and templates |
| 3. Component recipes | Per-component variants & states | CVA / Tailwind Variants / Panda recipe configs | Renderer, registry |
| 4. Editorial templates | Page-level slots, allowed block sets, density, ordering | Template schema + "degrees of freedom" config | Editor UI, AI page generator |
Each layer narrows the choice space available to the layer above it. A token is the smallest commitment; a template is the largest. The generator never reaches past its layer: it picks tokens only via recipe variants, and arranges components only inside template slots. That nesting is what keeps a billion possible pages all on-brand.
Design tokens are named design decisions: color.brand.600, space.4, radius.md, font.size.lg. They are the bottom of the guardrail stack because every other layer references them by name and never hard-codes a raw value. Change the token, and everything downstream moves in lockstep — and, critically, nothing outside the token set is reachable.
The standard finally stabilized. On 28 October 2025 the W3C Design Tokens Community Group published the Design Tokens Format Module version 2025.10, its first stable release — a vendor-neutral JSON format developed by 20+ editors from Adobe, Google, Amazon, Microsoft, Meta, Figma, and Shopify (w3.org DTCG announcement). Key capabilities relevant to a CMS:
$type and $value (color, dimension, fontFamily, fontWeight, duration, cubicBezier, number, plus composite types like shadow, typography, border, transition, gradient)."$value": "{color.brand.600}"), enabling semantic layering (raw palette → semantic role → component token).A canonical DTCG token file fragment:
{
"color": {
"brand": {
"600": { "$type": "color", "$value": { "colorSpace": "oklch", "components": [0.55, 0.21, 264] } }
},
"text": {
"default": { "$type": "color", "$value": "{color.brand.600}" }
}
},
"space": {
"4": { "$type": "dimension", "$value": { "value": 1, "unit": "rem" } }
}
}
Build pipeline — Style Dictionary. Style Dictionary v4 has first-class DTCG support and transforms one token file into platform-specific outputs: CSS custom properties, Tailwind theme, iOS Swift, Android XML, Flutter, JSON for the editor UI. (Note: as of early 2026, full support for the exact 2025.10 format is still landing in Style Dictionary v5 — most teams run v4 with the stable subset.) Tokens Studio for Figma and Terrazzo are the other major reference implementations, and Penpot, Sketch, Framer, Supernova, and zeroheight all read/write the format (styledictionary.com/info/dtcg).
Runtime — Tailwind CSS v4 @theme. Tailwind v4 (released 2025) moved configuration out of tailwind.config.js into a CSS-native @theme block. Each token becomes a namespaced CSS variable and generates utilities automatically: declaring --color-brand-600 creates bg-brand-600, text-brand-600, border-brand-600, while --font-display creates a font-display utility and --breakpoint-3xl creates a 3xl: variant (tailwindcss.com/docs/theme).
@theme {
--color-brand-600: oklch(0.55 0.21 264);
--color-text-default: var(--color-brand-600);
--space-4: 1rem;
--radius-md: 0.5rem;
}
Because v4 exposes all tokens as CSS variables at runtime, you can re-theme by swapping a [data-theme="..."] attribute with no rebuild — multi-brand, dark mode, and per-tenant theming for free (tailwindcss.com/blog/tailwindcss-v4). The DTCG file is the source of truth; Style Dictionary emits the @theme block; Tailwind generates the utilities; the recipe layer is the only thing allowed to use those utilities. That chain is the first guardrail: the AI can only color text with colors that exist as tokens.
Do not let recipes reference raw palette tokens directly. Use three tiers:
color.blue.600, space.4. The palette. No meaning.color.text.default, color.surface.raised, color.border.subtle. Roles. This is the layer recipes consume.button.primary.bg. Per-component overrides for edge cases.The semantic tier is the real guardrail surface. When the generator asks for "a warning callout," it gets color.feedback.warning.* — and dark mode, high-contrast, and a future rebrand all flow through that one alias. The model never sees yellow.400.
Tokens constrain values; primitives constrain spatial relationships. The Every Layout canon (every-layout.dev/layouts) and production systems like Atlassian's primitives (atlassian.design spacing primitives) and Braid/Seek reduce all of layout to a handful of single-responsibility components, each doing exactly one thing:
| Primitive | Single responsibility | Token it consumes |
|---|---|---|
| Stack | Vertical rhythm — consistent space between children | space.* |
| Cluster | Wrap a row of items with even gaps (tags, button rows) | space.* |
| Grid | Responsive equal-width columns that reflow without media queries | space.*, min item width |
| Sidebar | Two-column where one is fixed-ish and one fills | space.*, sidebar width |
| Switcher | Switch from horizontal to vertical below a content-driven threshold | space.* |
| Cover | Vertically center content with header/footer (hero) | min-height |
| Box / Frame | Pad evenly; enforce aspect ratio | space.*, ratio tokens |
| Center | Constrain measure (line length) and center horizontally | max-measure token |
The power move: these primitives encode responsiveness intrinsically, using flex-wrap, min(), clamp(), and container queries instead of breakpoints. The generator cannot produce a layout that breaks on mobile because the primitive handles reflow itself (every-layout.dev). A <Stack space="4"> is always correctly spaced; there is no way to ask for an arbitrary margin-top: 37px.
// The editor/AI emits structure, never raw CSS:
<Stack space="6">
<Heading level={2}>{title}</Heading>
<Center measure="prose">
<Prose>{body}</Prose>
</Center>
<Cluster space="3">{tags.map(t => <Tag>{t}</Tag>)}</Cluster>
</Stack>
By 2025 <Stack> had become the canonical "first layout primitive everyone reaches for" (dev.to layout primitives). Container queries (now baseline-supported across browsers) make primitives even stronger: a component reflows based on its own width, so the same <Grid> works in a wide article body and a narrow sidebar without the generator knowing or caring where it landed.
Recipes turn a component's design decisions into a typed, finite set of variants. The dominant 2026 tools:
| Tool | Model | Best for | Notes |
|---|---|---|---|
| CVA (class-variance-authority) | Runtime, framework-agnostic | Simple, portable variant logic | variants, compoundVariants, defaultVariants; powers shadcn/ui (cva.style) |
| Tailwind Variants | Runtime, Tailwind-first | Responsive variants, split components, slots | First-class slots + compound slots (dev.to CVA vs TV) |
| Panda CSS recipes / slot recipes | Build-time, atomic CSS | Type-safe design systems, lean CSS | cva/sva; config recipes ship only used styles; pre-generates variant combinations (panda-css.com slot recipes) |
A CVA recipe defines the entire legal surface of a button:
const button = cva("inline-flex items-center font-medium rounded-md transition", {
variants: {
intent: { primary: "bg-brand-600 text-white", subtle: "bg-surface text-default", danger: "bg-danger-600 text-white" },
size: { sm: "h-8 px-3 text-sm", md: "h-10 px-4", lg: "h-12 px-6 text-lg" },
},
compoundVariants: [{ intent: "primary", size: "lg", class: "shadow-md" }],
defaultVariants: { intent: "primary", size: "md" },
});
This is a guardrail because intent can only be primary | subtle | danger. The AI cannot invent a "neon" button; TypeScript rejects it and the renderer ignores unknown values. The variant matrix is the contract: the model fills in a slot from an enumerated dropdown, not a free-text style.
Slots (Nathan Curtis on slots) are the controlled-flexibility mechanism inside a component. A Card exposes named slots (media, header, body, actions); the editor can place allowed components into each, but cannot break the card's internal layout, padding, or alignment. Panda's slot recipes pre-generate the CSS for all variant combinations across every slot as atomic classes, so a Card with media + header + body always spaces and aligns identically regardless of what fills it (panda-css.com slot recipes). Slots give "an opportunity to position layout rules, structure, and visual styling at many reusable, independently managed levels" (Nathan Curtis) — flexibility within a frame, never outside it.
Distribution — shadcn-style registries. shadcn/ui reframed components as open code you own rather than an npm dependency: "not a component library — it is how you build your component library" (ui.shadcn.com/docs). The 2025 CLI 3.0 added namespaced registries (@registry/component), private registries with authentication, multi-file registry.json composition via shadcn build, and an MCP server so an AI agent can search and install components on its own (shadcn CLI 3.0 + MCP changelog). For an AI-native CMS this matters twice: (1) the components are open code an LLM can read and extend, and (2) the registry is itself a guardrail catalog — a closed, versioned set of approved blocks the generator may use, discoverable over MCP. registry.directory and registry.json schemas have made "your design system as a registry" a standard pattern.
The top layer governs the page: which blocks may appear, in what order, how many, and how dense. This is where you tune the knob between "rigid" and "anything goes." The industry consensus is the hybrid page:
"Hybrid pages specify some fixed content and allow some specific sections of the page to be dynamic and reordered… the most common type… gives the content author the right amount of flexibility without being too complex or tedious to edit." — Rangle Headless CMS Playbook (tbw.rangle.io)
A template defines slots with constraints expressed as schema:
template: article
slots:
- name: hero
required: true
allow: [HeroSplit, HeroCentered, HeroMinimal] # closed set
max: 1
- name: body
allow: [Prose, PullQuote, ImageFull, Gallery, CalloutInfo, CalloutWarning, EmbedVideo]
min: 1
max: 40
rules:
- "no two ImageFull in a row" # density / rhythm rule
- "Gallery requires >= 3 images"
- name: cta
allow: [CtaBanner, NewsletterBox]
max: 1
freedom:
reorder: body # body blocks reorderable
locked: [hero, cta] # position fixed
Three degrees-of-freedom dials, drawn directly from headless-CMS modular-content practice (Hygraph content modeling, eight25media modular modeling):
allow whitelist). The single most important guardrail: the generator can never reach for a block that isn't on the list.A critical modeling discipline: layout options and display variants are configuration, not content. Mixing the two "creates model complexity that compounds" (tbw.rangle.io best practices). The content model stores what (the words, the image, the author); the template/variant config stores how it looks. Keeping them separate is what lets the same content render as a feature, a card, or a list item without duplication — and lets you A/B or re-theme without touching content.
Beyond fixed templates, the strongest AI-native pattern is letting the content choose its own layout from a bounded set. A block carries a variant field (or the generator/renderer derives one from the content's shape):
Hero with a 16:9 image → HeroSplit; portrait image → HeroCentered; no image → HeroMinimal.PullQuote; longer → inline Blockquote.ImageFull; 3+ → Gallery; 2 → ImagePair.The mapping from content shape to variant is a pure function over a closed set of outputs — so two articles about different topics get visually different but equally valid pages. Best practice from the field is to keep this small: limit to 2–3 variants per section (Hygraph). More than that and editors/AI can't reason about the choice and the system loses coherence.
To break "every card looks identical," you can introduce controlled non-determinism — but never over raw CSS values. Randomize only over token-valid choices, seeded for stability:
Math.random(). Deterministic — the same article always renders the same way, so caching, SSR, and reader memory stay intact. variant = palette[hash(id) % palette.length].{brand, accent, neutral}, an image crop from {wide, square}, a card order within an allowed permutation set. Every outcome is, by definition, on-brand because every option came from a recipe variant or a template's allow list.Constrained randomness layered on content-driven variants is what makes an AI-generated section feel hand-arranged: the variety is real, the floor of quality is guaranteed, and nothing requires human review because nothing illegal can occur.
When an LLM (or editor) builds a page in this system, its entire output is a structured document of legal moves:
{
"template": "article",
"slots": {
"hero": { "block": "HeroSplit", "props": { "image": "...", "headline": "..." } },
"body": [
{ "block": "Prose", "content": "..." },
{ "block": "CalloutInfo", "props": { "tone": "info" }, "content": "..." },
{ "block": "Gallery", "props": { "columns": 3 }, "images": ["...","...","..."] }
],
"cta": { "block": "NewsletterBox", "props": { "variant": "subtle" } }
}
}
Notice what the model cannot emit: no class names, no inline styles, no colors, no spacing, no HTML. It chooses blocks from a whitelist, sets props from enumerated variants, supplies content. The renderer maps each block to a recipe + primitives + tokens. Validity is structural, not reviewed. A JSON Schema (one per template) can reject any illegal page before it renders — the same schema doubles as the tool definition you hand the model. This is the entire value proposition of treating the design system as guardrails: the surface area for an AI to be wrong shrinks from "all of CSS" to "a few dropdowns."
@theme for runtime, rebuild-free theming.color.text.default, never raw palette values; this is the layer dark mode and rebrands flow through.clamp(), min(), and container queries — the generator cannot produce a layout that breaks on mobile.$type/$value, aliases, composite tokens.@theme directive: tokens as CSS variables that generate utilities at runtime.variants, compoundVariants, defaultVariants API used by shadcn/ui.