---
title: "A design system for agents has to say no"
description: "A field report on making a design system enforceable by agents. Component contracts pair portable meaning with per-platform bindings, instance grammar defines the DOM each component requires, and a validator rejects markup that carries approved class names on the wrong structure."
content_type: "note"
canonical_url: "https://mantasvaitukaitis.com/docs/notes/design-system-for-agents"
markdown_url: "https://mantasvaitukaitis.com/docs/notes/design-system-for-agents.md"
author: "Mantas Vaitukaitis"
published_at: "2026-07-28T15:06:00.000Z"
updated_at: "2026-08-12T09:00:37Z"
topics:
  - "design systems"
  - "agents"
  - "engineering"
  - "design"
language: "en"
---

# A design system for agents has to say no

> An agent followed my design system and shipped a broken interface.

## Abstract

A field report on making a design system enforceable by agents. Component contracts pair portable meaning with per-platform bindings, instance grammar defines the DOM each component requires, and a validator rejects markup that carries approved class names on the wrong structure.

## What shipped

An interface built on the system shipped this settings trail.

```html
<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>
```

_page.html · the approved class name on markup the component never described_

The contract wanted a named landmark holding an ordered list, each item owning its link and a chevron. What shipped was a row of siblings with a typed slash.

A screen reader finds no list, no order, no current page. Grep the codebase for `ds-crumbs` and the component looks adopted.

A person writing that markup brings context the documentation never stated. An agent brings compliance. It builds what the guidance described, reports success, and by the only standard available to it, the work is done.

So the question stopped being what the system tells an agent. It became what the system can resolve, and what it can refuse.

## What I am building

A design system for agentic workflows. It installs into a project as an agent skill. Running the scaffold writes 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 did much of the writing, working against the system's own rules. Its failures arrive the same way. A real build goes wrong, and the fix becomes a condition the next build cannot skip.

## Resolve

Every component ships a contract. A portable root carries meaning: slots, axes, states. A binding carries one platform's implementation of it.

```json
"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]" }
}
```

_Button.json · rendered by design-system/web/components.css_

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.

No guessing. No prose about spinners.

The same split places behaviour. CSS can prove a button's states. A dialog needs focus trapping. A command menu needs filtering and arrow-key navigation. Those get contracted here and implemented in a runtime layer. A README cannot trap focus.

Styles bind to tokens, so rebranding a project leaves component code untouched.

```json
"root.bg":          "action.primary.bg",
"root.text":        "action.primary.text",
"root.bg@hover":    "action.primary.hover",
"root.bg@disabled": "action.disabled.bg"
```

_Button.json · ramps change, roles re-derive, one exporter regenerates the adapters_

## Refuse

The settings trail is why instance grammar exists. Each contract now carries one: which element, which children, in which order, carrying which attributes.

```json
"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 } ] }
}
```

_Breadcrumbs.json · same trail, one visible difference_

Every accessibility decision a person supplies from experience becomes 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.

```sh
$ 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).
```

_terminal · four rounds, no human in the loop_

The agent knew nothing about breadcrumb accessibility. It needed a grammar and something that says no.

## Where the rules come from

The checks that were easy to think up 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. I caught it in the browser, which costs more than catching it in the build.

```sh
$ 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%
```

_terminal · the failing line from a real run_

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.

A check nobody has watched fail is not yet a check.

That loop runs the whole repository. A decision starts in a token, a contract, or the reference implementation. The validator turns it into a condition. A negative test proves the condition can fail. The next agent inherits the correction instead of a longer prompt.

No future agent will hear about the selection-state bug. It will be unable to ship it.

## What is still ahead

Runtime components. Composable section recipes. 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 one class of wrong answer impossible to ship.
