00 — Design tokens

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.

How a design token works — one name, one place to change
Without tokens
button.tsx#e02424
modal.tsx#e12323
badge.module.css#de2020
Three files, three slightly different reds. Drift compounds silently over time.
With tokens
color.danger.boldoklch(0.60 0.22 27)
Consumed by
Delete button
Danger badge
Error alert
Inline error

Toggle the theme — all four update from one value change.

Without tokens, the same colour is written in dozens of files and drifts over time. With a token, every component reads from the same named source — and switching themes means changing the value, not hunting the codebase.
Why teams adopt them
  • 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.

Compared to other conventions
ApproachExampleTrade-off
Hardcoded valuesbackground: #e02424No abstraction layer. Drift is invisible until it's everywhere.
Unnamed CSS variables--color-1: #e02424Avoids repetition but carries no semantic meaning. Which button is `--color-1`?
Utility classesclass="bg-red-600"Fast to write. No single source of truth — rebrand means find-and-replace across every file.
Named design tokenscolor.danger.boldSemantic, themeable, consistent. The name describes purpose; the value can change freely.
Where tokens need to evolve

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.

The experiment · three briefs · names-only context · expected vs actual
  1. Brief 1

    Confirm a file was saved.

    ExpectedToast
    ActualAlert

    Alert persists until dismissed. A save confirmation should disappear on its own — but the name carries no information about persistence.

  2. Brief 2

    Warn the user their free trial ends in 3 days.

    ExpectedBanner
    ActualToast

    Toast auto-dismisses in seconds. The user needs to see this across the entire app. The name 'Toast' carries no information about scope.

  3. Brief 3

    Ask the user to confirm deleting their workspace.

    ExpectedDialog
    ActualAlert with buttons

    Alert doesn't block interaction — the user can dismiss it and proceed anyway. Dialog requires acknowledgment before anything else happens.

These aren't model errors — they're documentation gaps. Any frontier model given only component names will make the same class of mistakes. The names don't carry the information needed to choose correctly. That's the condition the experiment starts from.

Module 01

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.

Drift DS · six components at names-only context
  • Toastephemeral

    Floating notification, auto-dismisses

  • Alertinline

    Inline message block within page content

  • Bannerpersistent

    Full-width bar pinned to the top of the app

  • Dialogblocking

    Modal overlay, blocks all other interaction

  • InlineMessageinline

    Small hint beneath a form field

  • Lozengeinline

    Small 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.


Module 02

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.

Experiment · pick a brief, move between context levels
Choose a brief
"I need to confirm to the user that their file has been saved."
Context given to AI
System context · Names only
Toast:

Alert:

Banner:
Brief

I need to confirm to the user that their file has been saved.

AI

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
Correct is Toast
Rendered output

Alert

I need to confirm to the user that their file has been saved…

Left panel: what the model received. Right panel: how it reasoned and what it chose. The rendered output shows why the wrong pick matters — it's a different behaviour, not just a different label.

Module 03

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.

Three ambiguous pairs — what the name shares and what it doesn't
  • ToastvsAlert
    What the name shares

    A small message component

    What the name doesn't say

    Ephemeral vs persistent. One disappears in seconds; the other stays until dismissed.

  • AlertvsBanner
    What the name shares

    A persistent notification

    What the name doesn't say

    Page-scoped vs app-wide. Alert disappears when you navigate; Banner follows you.

  • BannervsDialog
    What the name shares

    Something the user needs to act on

    What the name doesn't say

    Advisory 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 model isn't confused. It's reasoning correctly from incomplete information. The fix isn't a better model — it's more specific information.

Module 04

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.

Before and after — description vs intent clause
Toast
L2 · Description only

A brief notification that appears and disappears automatically.

L3 · + Usage intent

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.

L2 → L3 fixes the Toast/Alert confusion on the 'file saved' brief.
Banner
L2 · Description only

A persistent, full-width message that stays visible across page navigation.

L3 · + Usage intent

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.

L2 → L3 fixes the Toast/Banner confusion on the 'trial ending' brief.
Dialog
L2 · Description only

A modal overlay that requires the user to take an action before continuing.

L3 · + Usage intent

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.

L2 → L3 fixes the Alert/Dialog confusion on the 'delete workspace' brief.
The intent clause is usually one sentence longer than the description. The accuracy jump is not marginal.
Most design system docs are written for humans who already have the intuition. The description confirms what they already suspect. The when-to-use clause is what's missing — it's the one thing an AI needs that most designers don't, because most designers don't start from a blank prior.

Module 05

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.

Three do-not clauses — what they rule out and why it works
Toast· do_not

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.

Why this works

This sentence rules out two entire component categories — Banner and Dialog — from the Toast namespace. The positive clause couldn't do that.

Alert· do_not

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.

Why this works

Pointing at the alternatives turns a vague don't into a routing decision. The model now knows the rule and the right exit.

Dialog· do_not

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.

Why this works

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.

One do-not per component is usually enough. More than that and you're describing failure modes that haven't happened yet — which is noise, not signal. Write them from real mistakes: every do-not in a healthy design system should have a story behind it.

Module 07

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.

Three formats · same four fields
---
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      |
What most teams actually ship vs what works
Typical DS documentation
Toast

A brief notification component.

StorybookControls

No when-to-use. No do-not. Great for human exploration; opaque to AI agents.

Context-layer documentation
Toast

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 context layer doesn't need to be comprehensive to be useful. Three components with good when-to-use and do-not clauses outperform fifty components with descriptions alone. Start with the components where AI gets it wrong — those are the ones that need it most.

Module 08

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.

Three composition failures — correct picks, wrong assembly
  1. containment rule
    Brief

    While the user confirms deleting their workspace, also surface their trial expiry warning.

    AI assembled

    Banner nested inside Dialog as a child component

    What broke

    Banner is layout-level. Inside a modal it's invisible to users who never trigger the confirmation — and disappears when the Dialog closes.

    Missing rule

    Banner: cannot_be_child_of Dialog App-wide conditions belong at the layout root, not inside a scoped overlay.

  2. region rule
    Brief

    After file upload completes, confirm success and show a storage usage warning.

    AI assembled

    Toast + Alert rendered in the same bottom-right region, identical visual treatment

    What broke

    Shared position implies shared behaviour. Users expect both to auto-dismiss and miss the persistent storage warning.

    Missing rule

    Toast: cannot_share_region_with Alert Different persistence models must occupy distinct visual zones.

  3. position rule
    Brief

    Ask the user to confirm a high-stakes action while surfacing a related billing alert.

    AI assembled

    Alert rendered as a layout sibling, behind the Dialog backdrop

    What broke

    The modal backdrop covers the Alert visually and interactively. The billing information is inaccessible while the Dialog is open.

    Missing rule

    Alert: when co-appearing with Dialog, must be placed inside Dialog body — not as a layout sibling covered by the backdrop.

Each brief was given full L4 context per component. Each component was chosen correctly. The failure came from how they were assembled.

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.

Extending the intent layer with composition rules
# 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).
The relationships block adds cross-component constraints in the same spirit as do_not — written from real failure modes, annotated with reasons.
This is the line between scaling consistency and scaling design intelligence. A design system with component intent tells AI what each piece means. One with composition rules tells it how the pieces fit. The intent layer is the prerequisite; the composition layer is where the next accuracy cliff lives.

09 — What changes

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.

The three fields that move the needle
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