A design token is a named design decision.
Instead of writing #e02424 in a stylesheet, a team writes color.danger.bold. That name is a contract: it says what the colour means, not what it currently looks like. The value behind it can change — for a different theme, a different brand, a different platform — and every component that uses the token updates automatically.
Design tokens are now the standard way large teams manage design decisions. Atlassian, GitHub, Adobe, Salesforce — and thousands of smaller teams — use them to keep colours, spacing, and typography consistent across codebases, themes, and platforms.
button.tsx#e02424modal.tsx#e12323badge.module.css#de2020color.danger.boldoklch(0.60 0.22 27)Toggle the theme — all four update from one value change.
Single source of truth
Change a value once, and every component that references it updates. No hunting through codebases for every hardcoded hex.
Theming without rewrites
Light mode, dark mode, high-contrast, brand variants — each is a different set of primitive values behind the same token names.
Design–code alignment
When designers and engineers use the same named tokens, there's no translation layer. The Figma variable and the CSS variable are the same decision.
Scalable governance
A 5-person team and a 500-person organisation both enforce consistency the same way — by making the right option the named one.
| Approach | Example | Trade-off |
|---|---|---|
| Hardcoded values | background: #e02424 | No abstraction layer. Drift is invisible until it's everywhere. |
| Unnamed CSS variables | --color-1: #e02424 | Avoids repetition but carries no semantic meaning. Which button is `--color-1`? |
| Utility classes | class="bg-red-600" | Fast to write. No single source of truth — rebrand means find-and-replace across every file. |
| Named design tokens | color.danger.bold | Semantic, themeable, consistent. The name describes purpose; the value can change freely. |
Design tokens were designed to be read by humans and consumed by build tools. Both readers bring context. A designer reading color.danger.bold knows it belongs on a delete button, not a promotional banner. A build tool doesn't care — it just resolves the value.
AI agents are a third reader, and they work differently from both. They reason from the token names and whatever documentation is available — but they start with no accumulated context about the system. Given only component names, they fall back on how those words appear across the internet: Stack Overflow, UI libraries, changelogs, tutorials. The result is predictable, systematic error.
The fix isn't a better model. It's a better intent layer — the part of a design token system that describes not just what a token is, but when to use it and, crucially, when not to. That layer has always existed in the heads of the designers who built the system. For the AI era, it needs to be written down.
- Brief 1
“Confirm a file was saved.”
ExpectedToastActualAlertAlert persists until dismissed. A save confirmation should disappear on its own — but the name carries no information about persistence.
- Brief 2
“Warn the user their free trial ends in 3 days.”
ExpectedBannerActualToastToast auto-dismisses in seconds. The user needs to see this across the entire app. The name 'Toast' carries no information about scope.
- Brief 3
“Ask the user to confirm deleting their workspace.”
ExpectedDialogActualAlert with buttonsAlert doesn't block interaction — the user can dismiss it and proceed anyway. Dialog requires acknowledgment before anything else happens.
The component library — six components, all called 'notifications'.
Drift DS has six components that could plausibly handle a message to the user. From names alone, you can't tell which one belongs where. That's the setup.
Before the experiment: the cast. These six components cover the full space of “something the user needs to know.” They differ in persistence, scope, prominence, and whether they require action. None of that is in the name.
What an AI reads when given this list with no additional context: six words. What a designer reads: six years of accumulated intuition about when each one is right. The experiment is about closing that gap.
ToastephemeralFloating notification, auto-dismisses
AlertinlineInline message block within page content
BannerpersistentFull-width bar pinned to the top of the app
DialogblockingModal overlay, blocks all other interaction
InlineMessageinlineSmall hint beneath a form field
LozengeinlineSmall status pill / badge
The four roles — ephemeral, persistent, blocking, inline — are the real taxonomy. But that taxonomy isn't in the names. Toast, Alert, and Banner all sound like variations on the same thing. The experiment will show what it takes to get an AI to understand the difference.
The experiment — context added one layer at a time.
Three briefs. Four levels of context. At each level, the model's reasoning is shown exactly as it would appear in a production AI coding tool — and the component it picks is rendered so the consequence is visible, not just named.
The experiment holds the briefs constant and varies only what the model knows about the design system. At level one, it has component names. At level four, it has a full usage spec. The reasoning shown at each level reflects the structure of what a frontier model actually produces — not paraphrased, but representative of the pattern.
The rendered output matters: when the wrong component is picked, it's not just the wrong name. It's a fundamentally different interaction — one that persists when it should disappear, or disappears when it should stay, or allows dismissal when it should require acknowledgment.
Toast: Alert: Banner:
I need to confirm to the user that their file has been saved.
This is a confirmation message. I'll use Alert — it's a message component, and it sounds right for communicating status to the user.
Alert
I need to confirm to the user that their file has been saved…
Why names fail — the information a name doesn't carry.
Toast. Alert. Banner. Three names, three distinct behaviours, zero overlap in the name. An LLM trained on the whole internet has seen all three — the question is which training distribution it's drawing from.
Component names are identifiers, not descriptions. Toast comes from Android's notification system. Alert comes from browser window.alert() and Bootstrap. Banner comes from marketing. All three have different implied semantics from their origin systems — and the LLM's prior for each one is whatever it absorbed from millions of Stack Overflow posts, UI libraries, and changelogs.
When you give the model only a component name, it doesn't read your system — it reads its own history with that word. That's why the errors are systematic, not random.
ToastvsAlertWhat the name sharesA small message component
What the name doesn't sayEphemeral vs persistent. One disappears in seconds; the other stays until dismissed.
AlertvsBannerWhat the name sharesA persistent notification
What the name doesn't sayPage-scoped vs app-wide. Alert disappears when you navigate; Banner follows you.
BannervsDialogWhat the name sharesSomething the user needs to act on
What the name doesn't sayAdvisory vs blocking. Banner can be ignored; Dialog prevents all other interaction.
At context level 2 — names plus one-line descriptions — the model does marginally better. But descriptions tend to describe appearance or general function: “a brief notification,” “a message displayed to the user.” These are still too similar to disambiguate the hard cases. The information that actually breaks the tie isn't what the component is — it's when you're supposed to use it, and when you're not.
The intent layer — when-to-use does more work than description.
At level 3, I added a single when-to-use clause per component. That one sentence got all three briefs right. Here's why one clause outperforms five sentences of description.
A description answers “what is this?” A when-to-use clause answers “when should I reach for this?” The second question is the one an AI agent is actually trying to answer when it's building a UI from a brief.
The when-to-use clause works because it's written in the same terms as the brief. “Ephemeral confirmations that don't require user action” matches directly to “confirm a file was saved.” “Product-wide messages the user must be aware of across their entire session” matches directly to “they need to see this across the whole app.” The model isn't inferring — it's matching.
A brief notification that appears and disappears automatically.
Use for ephemeral confirmations that don't require user action. The message disappears after a few seconds. Do not use for messages the user must read before continuing.
A persistent, full-width message that stays visible across page navigation.
Use for product-wide messages the user must be aware of across their entire session — trial expiry, billing warnings, maintenance notices. Stays until the user explicitly dismisses it or the condition resolves.
A modal overlay that requires the user to take an action before continuing.
Use when an action is irreversible or high-stakes and requires explicit user acknowledgment. The user cannot interact with the rest of the application until they respond.
The do-not layer — negative examples constrain probability faster.
At level 4, I added a do-not clause. It's usually one sentence. It improved accuracy more than moving from L1 to L3 combined. Here's why it works.
Positive examples add information about where to go. Negative examples add information about where not to go. In probability terms: a positive clause shifts probability mass toward one option. A negative clause removes it from others. When the confusion is between two plausible options — Toast vs Alert, Alert vs Dialog — removing one from the candidate set is more efficient than strengthening the other.
This is the same reason RLHF uses human preference comparisons instead of absolute ratings. “A is better than B” carries more signal per annotation than “A is good.” The do-not clause is writing that kind of signal into your documentation.
Don't use Toast for warnings, errors that need acknowledgment, or anything that persists across navigation. If the user might miss it, it's not a Toast.
This sentence rules out two entire component categories — Banner and Dialog — from the Toast namespace. The positive clause couldn't do that.
Don't use Alert for transient confirmations (use Toast) or for messages that must persist across navigation (use Banner). Alert disappears when the user navigates away.
Pointing at the alternatives turns a vague don't into a routing decision. The model now knows the rule and the right exit.
Don't use Dialog for passive information (use Alert or Banner). Don't use it for low-stakes confirmations — overuse of Dialog trains users to click through without reading.
This catches the failure mode we saw at L2 — 'an Alert with confirm buttons.' The don't explicitly names it.
The do-not clause also has a practical effect for human readers: it names the trap. “Alert disappears when the user navigates away” is something a senior designer knows. A new hire doesn't — and will reach for Alert for persistent messages and wonder why users keep missing them. The same sentence serves both readers.
What good looks like — three ways to encode the context layer.
The content matters more than the format. MDX, YAML, and LLMS.txt all work. The question is whether your when-to-use and do-not fields are written at all.
The context layer doesn't need a new format — it needs the right fields in whatever format you already use. The three examples below show the same component spec in three different shapes. Pick the one that fits your toolchain.
What most design systems actually ship: a Storybook demo with a controls panel and no prose. Sometimes a one-line description. Rarely a when-to-use clause. Almost never a do-not. The tooling exists; the content is the gap.
--- title: Toast description: A brief notification that appears and disappears automatically. --- ## When to use Use for ephemeral confirmations that don't require user action. The message disappears after a few seconds. ## Do not use - For warnings or errors that need acknowledgment (use Dialog) - For messages that persist across navigation (use Banner) - When the user might miss it: if the message matters, it's not a Toast ## Props | Prop | Type | Default | |---------|-----------------------------------|-----------| | variant | 'default' \| 'success' \| 'error' | 'default' | | duration| number (ms) | 4000 |
A brief notification component.
No when-to-use. No do-not. Great for human exploration; opaque to AI agents.
Ephemeral confirmations. Auto-dismisses.
Use when: transient feedback, no action needed
Don't: errors, persistence, cross-navigation
Readable by humans and agents alike. Same effort to write.
The composition layer — relationship rules beyond component intent.
An AI with full intent context for every component can still assemble a structurally broken layout. The intent layer governs individual picks. The composition layer governs how picks fit together.
The four context levels in the experiment held the number of components constant at one. Most real briefs don't. When an AI composes a UI with multiple components — even with accurate when-to-use and do-not clauses for each — it still needs to know how they relate. Which can contain which. Which can co-appear. Where each must sit in the layout hierarchy.
These aren't component-level questions. A Banner's do-not clause can say “don't use for transient feedback.” It can't say “don't be a child of Dialog” — because that constraint involves another component. That's the composition layer: cross-component rules that the intent layer can't encode alone.
- containment ruleBrief
“While the user confirms deleting their workspace, also surface their trial expiry warning.”
AI assembledBanner nested inside Dialog as a child component
What brokeBanner is layout-level. Inside a modal it's invisible to users who never trigger the confirmation — and disappears when the Dialog closes.
Missing ruleBanner: cannot_be_child_of Dialog App-wide conditions belong at the layout root, not inside a scoped overlay.
- region ruleBrief
“After file upload completes, confirm success and show a storage usage warning.”
AI assembledToast + Alert rendered in the same bottom-right region, identical visual treatment
What brokeShared position implies shared behaviour. Users expect both to auto-dismiss and miss the persistent storage warning.
Missing ruleToast: cannot_share_region_with Alert Different persistence models must occupy distinct visual zones.
- position ruleBrief
“Ask the user to confirm a high-stakes action while surfacing a related billing alert.”
AI assembledAlert rendered as a layout sibling, behind the Dialog backdrop
What brokeThe modal backdrop covers the Alert visually and interactively. The billing information is inaccessible while the Dialog is open.
Missing ruleAlert: when co-appearing with Dialog, must be placed inside Dialog body — not as a layout sibling covered by the backdrop.
Relationship rules follow the same principle as when-to-use and do-not clauses — they encode knowledge that exists in the heads of experienced designers but isn't written anywhere an AI can read. The format is a natural extension: a relationships block added to the same component spec.
Three dimensions cover most of the cross-component constraints that produce visible failures: containment (what can parent what), region (what can share visual space), and position (where within a layout hierarchy). The examples below add these to the Banner spec from the previous module.
# components/banner.context.yml name: Banner description: > Full-width, app-wide message. Persists across navigation. when_to_use: > Trial expiry, billing warnings, maintenance notices. Stays until dismissed or condition resolves. do_not: > Don't use for page-scoped messages (use Alert). Don't use for transient feedback (use Toast).
A design system that can't answer “when should I use this?” is half documented.
Design systems have always been written for readers who already have the intuition. The when-to-use clause confirms what a designer suspects. The do-not clause names the mistake an experienced engineer already knows to avoid. Documentation for someone who doesn't know has traditionally felt like documentation for nobody.
That's changed. The reader without the intuition is now the AI agent composing UI from a brief. It generates more UI in a day than most teams review in a week. If the design system can't express usage intent in writing, the agent fills the gap from whatever it absorbed during training — Bootstrap conventions, Material defaults, whatever the most common answer to “how do I show a notification” happened to be in 2022.
The fix is not a new tool or a new format. It's three fields per component that most teams are already expected to write but rarely do. The experiment above shows where the accuracy cliff is. The work is on the other side of it.
descriptionWhat it is. One sentence.when_to_useWhen to reach for this component over a similar one. Written in terms of the scenario, not the component.do_notWhat not to use it for. Name the alternative. One sentence per trap.By Sid Bhattacharjee · Part of a series on foundational software concepts