A design system for agents has to say no
An agent followed my design system and built a broken interface.
What shipped
An agent built a settings page on my design system. Here is its breadcrumb trail.
<nav class="ds-crumbs">
<a class="ds-crumbs__link" href="/settings">Settings</a>
<span>/</span>
<a class="ds-crumbs__link" href="/settings/billing">Billing</a>
<span>/</span>
<span>Invoices</span>
</nav>The contract asked for a named landmark holding an ordered list, each step owning its link and its chevron. What came out is a row of siblings held together by a typed slash.
A screen reader finds no list in there, no order, and no current page. Grep the codebase for ds-crumbs and the component looks adopted.
Nothing about this was careless. A person writing that markup brings context the documentation never wrote down. An agent brings compliance. It built what the guidance described, checked it against everything it had, and by that standard the work was done.
The contract answered every question that had an answer, and the agent got all of those right: the class name, the variants, the states. The structure was the part it could only describe, and a description is something you can satisfy loosely.
Two things had to change: what the system can answer for an agent, and what it can stop one from doing.
What I am building
A design system for agentic workflows, installed into a project as an agent skill. Run the scaffold and you get a project-owned design-system directory: canonical tokens, component contracts, a framework-free CSS reference implementation, brand and icon manifests, a specimen, and the validator that holds all of it to account.
Agents wrote much of it, working against the system's own rules. They break it the same way. A real build goes wrong, the fix becomes a condition, and the next build cannot skip it.
What it answers
Every component comes with a contract. It is a JSON file that describes the component instead of styling it: the slots, the variants, the states. One half is portable and carries the meaning. The other half binds that meaning to one platform.
"class": "ds-button",
"axes": {
"variant": { "modifiers": {
"primary": "ds-button--primary",
"secondary": "ds-button--secondary",
"ghost": "ds-button--ghost",
"danger": "ds-button--danger" } },
"size": { "modifiers": {
"sm": "ds-button--sm",
"md": null,
"lg": "ds-button--lg" } }
},
"states": {
"disabled": { "driver": ":disabled, [aria-disabled=true]" },
"loading": { "driver": "[aria-busy=true]" }
}The agent reads variant=danger and resolves ds-button--danger. It reads "md": null and learns the default size carries no modifier, instead of inventing ds-button--md. It reads loading and gets [aria-busy=true], an attribute it sets and CSS answers.
Not one of those is a guess.
The same split decides where behaviour lives. CSS can prove a button's states on its own. A dialog needs focus trapping, a command menu needs filtering and arrow-key navigation, and those get contracted here and implemented in a runtime layer. A README cannot trap focus.
Styles bind to tokens, so rebranding a project leaves the component code alone.
"root.bg": "action.primary.bg",
"root.text": "action.primary.text",
"root.bg@hover": "action.primary.hover",
"root.bg@disabled": "action.disabled.bg"What it refuses
That settings trail is why instance grammar exists. Every contract now carries one: which element, which children, in which order, carrying which attributes.
"entry": "wrapper",
"parts": {
"wrapper": { "selector": "nav",
"attributes": { "aria-label": "Breadcrumb" },
"children": [ { "part": "root", "min": 1, "max": 1 } ] },
"root": { "selector": "ol.ds-crumbs",
"children": [ { "part": "item", "min": 2 } ],
"constraints": [ { "part": "item",
"attribute": "aria-current", "equals": "page",
"count": 1, "position": "last" } ] },
"item": { "selector": "li.ds-crumbs__item",
"cases": [
{ "when": { "aria-current": "page" },
"allowText": true, "children": [] },
{ "otherwise": true, "ordered": true,
"children": [
{ "part": "link", "min": 1, "max": 1 },
{ "part": "separator", "min": 1, "max": 1 } ] } ] },
"separator": { "selector": "svg.ds-crumbs__separator",
"attributes": { "aria-hidden": "true" },
"children": [
{ "part": "separator-icon", "min": 1, "max": 1 } ] }
}Every accessibility decision a person supplies from experience is now a structural requirement. The landmark and its label. The ordered list. One current-page marker, and it has to be last. The separator hidden from assistive tech.
A class name is easy to type onto anything. A structure is not.
The correction loop
Run the validator against the consumer project. It walks the real markup and answers in file and line.
$ validate_design_system.py --root . --consumer-drift
ERROR: page.html:3: Breadcrumbs root must match ol.ds-crumbs
# wrap it in a named landmark, carry the trail on an ordered list
ERROR: page.html:3: Breadcrumbs root requires 2+ item child; got 0
ERROR: page.html:3: Breadcrumbs root requires exactly 1 item with aria-current='page'
ERROR: page.html:4: Breadcrumbs root has uncontracted child <a>
ERROR: page.html:5: Breadcrumbs root has uncontracted child <span>
# give each step its own item, mark the current page
ERROR: page.html:5: Breadcrumbs item has uncontracted child <span>
ERROR: page.html:5: Breadcrumbs item requires 1..1 separator child; got 0
# replace the typed slash with the contracted icon
Result: passed with 0 warning(s).The agent knew nothing about breadcrumb accessibility, and it never had to. It needed a grammar, and it needed something that would say no.
Where the rules come from
The checks that were easy to think of came first: contrast ratios, required tokens, the shape of a contract. The ones worth having came from real builds going wrong.
A selection state shipped as a pale tint in light mode and a solid block in dark. Same colour family, both modes declared, every check green. Nothing in the build could see it. I found it by looking at the screen, which is the opposite of the point.
$ validate_design_system.py --root .
ERROR: selection.bg emphasis is asymmetric across modes: 1.13:1 from surface.canvas in light and 1.72:1 in dark; keep subtle-fill mode contrast within 30%The check had been asking which ramp the colour came from. It never asked how far the step landed from its own background. Now it measures both modes and compares them.
The tolerance came from measuring the roles that already worked.
Two tests keep it honest. One feeds the validator the broken pair and expects it to fail. The other checks that every shipping role still passes. Until you have watched a check fail, you do not know that it works.
That loop runs across the whole repository. A decision starts as a token, a contract, or a line in the reference implementation; the validator turns it into a condition, and a negative test proves the condition can fail. What the next agent inherits is the correction itself, not a longer prompt.
No future agent will hear about the selection-state bug. It just will not be able to ship it.
What is still ahead
Runtime components. Composable patterns. A query surface that answers system questions in canonical JSON. Everything above this line runs today.
None of it teaches an agent design. It makes that breadcrumb trail impossible.