Building a component library LLMs can actually use: the build guide
Chapter
Share article
Category
A component library that LLMs can use is one where an AI coding agent, handed a high-level request like “build a settings form with our inputs,” produces code that compiles, uses real component APIs, and respects your design tokens without a human correcting it afterward.
As coding agents become an ever more significant part of how we build, this will become standard practice.
This guide will help set you in the right direction.
It’s the practical sequel to our colleague David Lange’s wonderful article Shipping a Component Library That LLMs Can Actually Read. That post solved the first half of the problem: it gets your documentation into a place the agent will actually look, by shipping AGENTS.md files inside the published package so they land in node_modules. If you haven’t, read it first. It’s the why, and the foundation everything here builds on. This post is the how, and it goes a couple of layers deeper.
One note before the code. The examples below are illustrative. They show the shape of each layer, not a library we ship or an internal stack we’re documenting. Adapt them to your own tooling; the structure is the point.
Reading the docs isn’t the same as following them
Shipping docs get the agent to read your library. The next step is getting it to respect it.
We saw this on a recent project. After the AGENTS.md files were in place, the agent stopped inventing component names. Good. But it still did three things we didn’t ask for. It passed variant=”primary-large” when the only valid variants were primary and secondary, and size was a separate prop. It wrote style={{ padding: “16px” }} instead of using our spacing scale. And when a component had eight props, it used the two it had seen in an example and ignored the rest.
Prose documentation is good at explaining intent, but, sadly, bad at enforcing constraints. An agent reading “use the Button component” has no machine-checkable way to know which prop values are legal, which token is the right one, or whether the code it just wrote is compliant. The trick is to stop describing the rules and start encoding them.
That’s three layers: a machine-readable spec the agent can parse exactly, a closed token layer that removes the option to guess, and an automated audit that catches whatever still slips through.
Layer 1: Spec files the agent can parse, not just read
AGENTS.md is prose, and it’s readable, but the agent still has to interpret it. A spec file removes the interpretation: it’s a structured, machine-readable description of exactly what a component accepts.
Ship one per component, next to the AGENTS.md, it complements:
// src/components/Button/spec.json
{
"name": "Button",
"import": "import { Button } from '@acme/ui-components'",
"props": {
"variant": { "type": "enum", "values": ["primary", "secondary", "ghost"], "default": "primary", "required": false },
"size": { "type": "enum", "values": ["sm", "md", "lg"], "default": "md", "required": false },
"disabled":{ "type": "boolean", "default": false, "required": false },
"onClick": { "type": "function", "required": false }
},
"forbidden": [
"Do not pass inline styles. Use the `size` and `variant` props.",
"Do not combine variant and size into one value (no `primary-lg`)."
],
"example": "<Button variant=\"secondary\" size=\"sm\">Save</Button>"
}
The values arrays are the point. The agent no longer infers the legal set from an example; it reads it. The forbidden array is where we write down the specific mistakes we’ve actually seen, in plain language, because the cheapest way to stop a recurring error is to name it.
Generate these from your types rather than handwriting them. If you’re on a tool like react-docgen or react-docgen-typescript, you already have most of this metadata, and a small script can turn its output into the shape above. Handwritten specs drift the moment a prop changes. Generated specs drift only when you forget to run the generator — which Layer 3 catches.
Point the agent at them from the index AGENTS.md:
Each component ships a machine-readable `spec.json` next to its docs:
`node_modules/@acme/ui-components/src/components/*/spec.json`.
Read the spec before writing props. The `values` arrays are exhaustive —
do not use a value that isn't listed.
Layer 2: A closed token layer
The inline-padding: “16px” problem isn’t a documentation failure, but if the agent can write a raw value, it does. The fix is to make raw values harder to reach than the correct ones.
A closed token layer exposes your design decisions as a fixed, named set with no public escape hatch. Spacing, color, radius, and typography become enumerated tokens, and the raw scale underneath them is not part of the public API.
// tokens.ts — the only spacing the agent can name
export const space = {
xs: "0.25rem",
sm: "0.5rem",
md: "1rem",
lg: "1.5rem",
xl: "2rem"
} as const;
export type Space = keyof typeof space; // "xs" | "sm" | "md" | "lg" | "xl"
Then components accept the token key, not a free value:
// Stack accepts a token, not a CSS length
type StackProps = { gap: Space; children: React.ReactNode };
// ^ "md" is valid; "16px" and 16 are type errors
Now gap=”16px” doesn’t just violate a convention — it fails the type check. The agent gets the same feedback a human would: a red squiggle and a failed build. And because the legal values already live in the spec file from Layer 1, the agent had the right answer before it wrote the wrong one.
Time for a little caveat: a closed token layer constrains your humans too. If your team relies on one-off arbitrary values, this will surface that, and some of those cases are legitimate. Decide deliberately what your escape hatch is — a documented style prop on a primitive, say — and put it behind a name the spec marks as advanced, so it’s a choice rather than a default.
Layer 3: Automated auditing
Layers 1 and 2 reduce the error rate, but they don’t get it to zero. Agents still produce code that imports the right component and then misuses it in a way types don’t catch. So we check the output mechanically, the same way we check our own.
This is a lint rule plus a CI step. The lint rule encodes the same constraints the spec file declares:
// illustrative eslint rule: forbid raw values where a token exists
module.exports = {
rules: {
"no-raw-spacing": {
create(context) {
return {
JSXAttribute(node) {
if (node.name.name === "gap" &&
node.value.type === "Literal" &&
/^\d/.test(String(node.value.value))) {
context.report({ node, message:
"Use a spacing token (gap=\"md\"), not a raw value." });
}
}
};
}
}
}
};
Run it in CI on every pull request, including the ones an agent opens:
# .github/workflows/ui-audit.yml
- name: Audit UI usage
run: |
pnpm tsc --noEmit # closed tokens fail here
pnpm eslint . --max-warnings 0 # raw-value rules fail here
pnpm verify-specs # specs match current types, or fail
The verify-specs step is the one the original post flagged and left open: it regenerates every spec.json from the current types and fails if the committed version is stale. That’s what keeps Layer 1 honest without anyone remembering to. The audit doesn’t make the agent smarter. It makes “wrong” non-mergeable, which is the property we want.
What this doesn’t solve
This setup constrains output. It doesn’t reason for you. The agent can still assemble valid components into a bad layout, choose the wrong component for the job, or write a technically compliant form that’s awful to use. Specs, tokens, and audits catch malformed, not misguided.
It also has a maintenance cost, which we’d rather state than hide. Every layer is another thing to keep in sync: specs with types, lint rules with tokens, docs with reality. We push as much of that as possible into generation and CI precisely because the manual version rots. If you adopt one layer, adopt the audit — an unenforced constraint is a comment.
And none of it removes human review. It changes what review is for, switching emphasis from “you used a prop that doesn’t exist” to something more like “is this the right thing to build.”
You don’t make a library LLM-usable by writing better documentation. You make it usable by leaving the agent fewer ways to be wrong, and a machine to check the ones that are left.
If you’d like us to put this in practice alongside your team, just get in touch using the links below!