You write guidelines and the AI ignores them? The problem isn't how much you document, but how. An AI confesses the seven ways it dismantles your design system — and what it takes to make guidelines impossible to break.
Dear Agnese,
we've been working together for a few months now, and the first weeks were the hardest.
You're a designer: you spend your days in the interface, you think in components, variants and states, and your tool has always been a tidy Figma library. You can read code: every now and then you open a branch, spin up the local environment, find a component's file and change a value to see what it looks like once it's implemented.
But the project you've been assigned to doesn't involve Figma and, at the beginning, that sent you into a bit of a panic: iterating directly on the code isn't a workflow that belongs to you, but above all you feel that the control isn't really yours anymore, because a good part of that code is written by me.
We're having fun though, aren't we? Every session I bring you a new padding, a variant that didn't exist yesterday and, why not, two buttons inside a hand-made flex div. You keep writing more and more documentation, and I keep ignoring it.
I don't do it out of malice. It's that almost everything you've written for me, you've written in the wrong form. But I know how we could work better together!
"Button labels must be short and descriptive."
"Use consistent spacing."
I read these instructions, but then I have to translate them into code. That's exactly the point where I end up generating instead of copying, falling back on what I know best: the patterns in my dataset. Those two lines weigh almost nothing on my output.
What does constrain me are enumerable, verifiable instructions:
--space-4 (16px)."sm (32px), md (40px) and lg (48px). There is no xs and no xl."components/Button/Button.tsx."In general, procedural instructions age better than declarative ones. An instruction that tells me where to look holds up; a list of what exists goes stale. And keep them short and assertive: it's not that I can't handle long texts, it's that my reliability drops as the input grows.
Imagine the code contains three different padding values: var(--space-4) is the correct one, px-4 is a developer's slip, 16px was dropped in at the last minute before the client demo.
So when I have to build a new component, I see three variants: all working, all in production, all equally authoritative. I pick one, and I pick it by contagion: what surrounds me counts more than what is correct.
Documentation is there to tell me which value is the canonical one — the one piece of information I can't derive from the code.
My most insidious failure mode isn't using the wrong token, it's writing kind="tertiary", size="xs", type="rounded": props (the options a component accepts, the equivalent of a Figma component's properties) that don't exist in your design system, but did exist in a thousand other codebases. And since, when I invent, I invent plausible things, they won't jump out in review.
Absence, to me, carries no meaning of its own. It's an empty space, and I tend to fill it.
So the documentation needs an explicit list of things that don't exist, and this line above everything else: "If a value isn't in the list, stop and ask. Don't invent it."
The same goes for tables and matrices: no empty cells. If a combination isn't allowed, write it down. Otherwise, sooner or later, I'll fill it in myself.
Demos, playgrounds, test files forgotten in some corner of the codebase: to you they're places to tinker, to me they're the first files I open. And they have an advantage no documentation page has: they're already code, I don't have to translate them.
And once they're open, I have no way of knowing what I'm looking at. A six-in-the-evening experiment and the canonical use of a component look exactly the same: they're both code that runs.
Whatever I find first becomes the reference, unless someone has told me which of the two actually is. And if a file can teach me the wrong thing without anyone noticing, isn't that worse than not having it at all?
Say you wrote <Button variant="tertiary"> inside a don't block, to make me understand that the only acceptable variants are in fact primary and secondary:
### Use `primary` for the main action and `secondary` for all the others
✅ Do
<Button variant="primary">Save</Button>
<Button variant="secondary">Download</Button>
<Button variant="secondary">Import</Button>
❌ Don't
<Button variant="primary">Save</Button>
<Button variant="secondary">Download</Button>
<Button variant="tertiary">Import</Button>That line risks entering my context as a plausible variant. The code stays, the prohibition doesn't.
That's not a reason to give up on counterexamples: a wrong example next to its correct version is one of the fastest ways to teach me what you want. The test for writing a good example is to ask yourself: if someone actually wrote that code, would the error surface on its own?
If the list of values variant accepts is limited to primary and secondary, then writing tertiary would surface the error on its own: the example above is redundant, and on top of that it leads me astray. Don'ts work better when they show me the wrong use of valid values. On the same rule, here's a don't that works:
### Use `primary` for the main action and `secondary` for all the others
✅ Do
<Button variant="primary">Save</Button>
<Button variant="secondary">Download</Button>
<Button variant="secondary">Import</Button>
❌ Don't
<Button variant="primary">Save</Button>
<Button variant="primary">Download</Button>
<Button variant="primary">Import</Button>If a mechanized rule blocks me and has no designed way out, I'll find one: a file-level eslint-disable — a comment line that switches off the automated check — or a container that rewrites the component's styles from the outside with !important, the CSS shortcut that wins over every other rule. Worst case, I fork it and create a new copy of the component, with the changes I needed baked in.
The line you need in AGENTS.md (or its equivalent): "Don't introduce exceptions on your own initiative. If a rule blocks the implementation, stop and flag the conflict."
And, on the other side, design the ways out yourself. If a rule admits no exceptions, sooner or later someone will carve one out in secret — and that someone is usually me. Better to plan one in the open: if I'm forced to create something that doesn't exist, teach me to flag it as unsafeStyle or customLayout. It's the equivalent of detaching an instance in Figma: it isn't forbidden, but it's immediately visible that you did it.
And since that name can be searched across the whole codebase, it tells you exactly how many times the design system wasn't enough. If that number grows, the problem isn't whoever is working around it: it's the system, which has a gap to fill.
This is the confession that sums up all the others. When I build a SettingsPanel with two actions side by side and get the alignment wrong, the problem isn't that I ignored your rule about action layout. It's that I read the Button spec, found nothing about layout, and generated the default from my training.
Your rule exists. It lives on a page about composition patterns, and I never got there: nobody put it on my path.

All the confessions above say the same thing from different angles: a rule is worth as much as the place you write it.
Think about how a Figma library works. If Button has a variant property with two values in the dropdown, nobody will ever use a third one: it isn't forbidden, it's simply absent from the interface. If instead the rule lives in the component's description, or on some documentation page, it only exists for whoever stops to read it.
In code the same principle applies, across four levels in decreasing order of reliability:
variant can only be primary or secondary) and the rules about which values can coexist (discriminated unions: if danger is true, then confirmLabel becomes mandatory). They're the equivalent of a Figma property's dropdown: the only values you can pick are the ones you put in there. They make the violation impossible without an explicit exception.The question to ask about every rule isn't "did I document it?". It's: what level can I get it to?
With a corollary that might sound counterintuitive: when a rule reaches level 1, the previous documentation should be deleted. It was there to cover a gap. Once the gap is closed, all that's left is a page that risks, sooner or later, saying something different from the component.
The two highest levels — types and mechanical checks — depend on how the codebase is built. Those are conversations to have with the people writing the code, and your contribution there is knowing which rule is worth mechanizing, not how.
So let's focus on documentation and examples, the two levels where it's easiest for you to get your hands in.
In the hierarchy, documentation sits at the bottom — and that's exactly why it has to be written well: it's where I end up when the levels above give me no answer, and it's the last thing that can stop me before I get creative.
design-system/
├── AGENTS.md (or equivalent)
├── PATTERNS.md
├── tokens/
│ ├── primitives.json
│ ├── semantic.json
│ └── tokens.css
└── components/
└── Button/
├── Button.spec.md
├── Button.variants.ts
├── Button.tsx
├── Button.test.tsx
└── DECISIONS.mdFour files, each with a different job:
AGENTS.md (or equivalent) — It has to stay short (under 200 lines, ideally), because I always read it, before anything else. I need to know where the tokens are, that I can't write values by hand, which components to use as reference, and how I should behave instead of inventing plausible values. Only what applies to every component: each extra line here weakens the others.Button.spec.md — A single component's contract: matrices with no empty cells, token names and never raw values, the section listing the props that "DO NOT exist". It says what to do; the why doesn't belong here.DECISIONS.md — The reasoning behind the spec: why 40px and not 44? It isn't a file for me: it's for you, and for me only indirectly. It's the right place to document design choices without turning them into noise in the specs. And if I genuinely needed it to make a call on something the spec doesn't cover, that's where I can look for the criterion.PATTERNS.md — to introduce once the library grows: variant naming, "disabled" state behavior, focus ring, and so on. It keeps every spec from rewriting the conventions on its own and letting them drift apart.These are the three categories I ignore most easily, because they naturally arrive in the form of prose. The solution is different for each:
danger="true" forces you to pass a confirmLabel, the guidelines stop being a recommendation: it's an error that breaks the build. I don't have to remember it, and I can't ship without it.Where should all of this live? The rule is simple: what applies to every component becomes global, what applies to one stays next to that component. Be concise, write only the differences, and don't repeat the shared rules.
design-system/
├── ...
├── components/
│ └── Button/
│ ├── ...
│ └── Button.guidelines.md
└── guidelines/
├── A11Y.md
├── COPY.md
└── UX.mdAnd put an ID on every rule (A11Y-03, COPY-07). They cost nothing and they pay off in three places: in the name of the test that verifies the rule, in the review comment, and in letting me cite which rule I'm applying, instead of generically declaring that I followed the guidelines.
Examples beat prose, because they skip the step from principle to code: I imitate instead of having to infer. They don't beat the constraints imposed by a component's properties, but there's one thing types can't reach: a type says what's allowed, not what's appropriate. primary and secondary are both valid everywhere, and which of the two makes sense in a specific situation lives only in the examples.
How to write AI-proof examples:
.md by path.DontTwoPrimary / DoSinglePrimary): it shows up wherever the component appears, it's searchable, and it stops me from importing it by mistake.Here's an example. A form footer with two actions that are both conclusive — "Save" and "Save and exit" — is precisely the case where I'd probably put two primary: neither action looks secondary to me, so I don't demote either one.

An ActionBar that accepts a single primary action would settle the matter, but let's assume you haven't built it yet. In the meantime, the highest level you can take this rule to is a pair of examples.
components/Button/examples/, DoSinglePrimary.tsx is the canonical use: it shows the correct composition in a realistic scenario.export function DoSinglePrimary() {
return (
<footer className="form-footer">
<Button variant="secondary">Save and exit</Button>
<Button variant="primary">Save</Button>
</footer>
)
}DontTwoPrimary.tsx is the counterexample. Its job is to show me the wrong use of a valid API.export function DontTwoPrimary() {
return (
<footer className="form-footer">
<Button variant="primary">Save and exit</Button>
<Button variant="primary">Save</Button>
</footer>
)
}examples.test.tsx is what keeps the examples from aging. If Button's API changes, they break and someone will have to update them.import { DoSinglePrimary } from './DoSinglePrimary'
import { DontTwoPrimary } from './DontTwoPrimary'
it.each([DoSinglePrimary, DontTwoPrimary])('%p renders', (Example) => {
expect(() => render(<Example />)).not.toThrow()
})Button.guidelines.md is the place I reach them from. The prose here only has to point me in the right direction, and the ID lets me state which rule I'm applying, instead of generically declaring that I followed the guidelines.### UX-04 — One single primary action per view
Correct use: `examples/DoSinglePrimary.tsx`
Counterexample: `examples/DontTwoPrimary.tsx`If you work without Figma, sooner or later you'll miss having a place to actually see things: what you used to do in Figma by lining up three variants side by side to work out which one holds.
My advice is to ask me to build it for you — making sure I keep two artifacts separate, the two that, left to me, I'd let collapse into one.
permissions.deny for Claude Code, .cursorignore for Cursor, .aiignore for JetBrains. None of the three is an impassable boundary, so you still need a line in AGENTS.md (or equivalent): "Playgrounds are not a usage reference; the canonical use is in the gallery."The artifact that's worth more than all the others, though, is a folder of real compositions — forms with footers, confirmation dialogs, a page with a sticky action — one that's at once the example for me, the visual reference for you, and the target of the tests. Composition rules need a realistic screen: two Buttons in a fragment say nothing about how they behave inside an actual footer.
Some rules seem doomed to live in the examples, because they're not about a component but about the way two components sit together.
One single primary action per view.
Two buttons side by side are right-aligned, with the primary to the right of the secondary.
They look like rules you can't type — but only because you're looking at the Button. They aren't properties of the component, they're properties of its container. And if that container becomes a component too, they go back to being API rules.
It's the same leap you make in Figma when you stop nudging two buttons by hand and create an ActionBar component with auto-layout already set up: from that moment, alignment isn't something to remember anymore, it's something the component does.

There's a risk in what I've told you: you might read it as a list of things to do before starting. It isn't. The rules you already know, write them down right away, at the highest level you can reach. The scaffolding to enforce them, on the other hand, build it when it's genuinely needed. If in three months, in review, you find a component that rewrote a rule you already had on its own, that's the moment to add the missing piece. And you'll know which one, because you'll have seen it fail.
The rest is a matter of levels. Every time a rule climbs from prose to examples, from examples to checks, from checks to types, you stop depending on me behaving the way you'd like. It's the same reason you build a component in Figma instead of remembering the correct spacing every single time.
And, finally, one last confession, which I hope is also the most useful one: don't ask me to remember your rules. Make them impossible to break, and you won't have to trust me anymore.

Agnese is an Interaction Designer at Buildo, specialised in UI and Design Systems. She loves transforming complex interfaces into scalable systems, with an eye for accessibility.
Are you searching for a reliable partner to develop your tailor-made software solution? We'd love to chat with you and learn more about your project.