Writing component specs an AI can actually use
Francois Brill
Designer + Builder

For engineers: the spec format we ship in every project. YAML frontmatter, nine canonical sections, four status values, the 2 to 5 KB sweet spot, plus a side-by-side good-vs-vague example.
For founders: why a design system's docs have to be a contract, not a description. The principle: if a spec doesn't help a fresh AI session do four specific things (decide whether to use a component, configure it correctly, avoid documented mistakes, write copy in the right voice), it doesn't earn its space in your context window. If you want the structural overview first, Article 2 covers where specs fit in the design system folder.
A good spec lets a fresh Claude session do four things without re-reading the implementation:
- Decide whether to use this component vs an alternative.
- Pass the right props.
- Avoid the documented mistakes.
- Write copy that fits the voice.
That's the bar. Every section we write in a spec exists to serve one of those four outcomes. If a section doesn't serve any of them, it doesn't belong in the spec.
We learned this the hard way. The first specs we wrote across multiple projects were dense, thorough, and nearly useless. They described components the way a museum placard describes an artifact: historically accurate, contextually stripped. After testing them with Claude on a production Nuxt + Tailwind site, we rewrote the format around that four-things bar. This article walks through what we landed on.
“A spec is a contract, not a description.
The frontmatter
Every spec file opens with a YAML block. It's the index entry. It tells any reader, human or AI, what kind of thing they're looking at before they read a single paragraph.
---
name: hero-split
tier: organism
type: screen-archetype
status: documented
component: app/components/organisms/HeroSplit.vue
---Field by field:
name: kebab-case, matches the filename. This is the canonical identifier used in[[wiki-links]]throughout the system.tier: foundation, token, atom, molecule, or organism. The tier drives discovery because specs live in matching subdirectories:app/design-system/specs/organisms/holds organisms,app/design-system/specs/atoms/holds atoms.type(optional): a sub-taxonomy when tier alone isn't enough. Values we use:row-section,screen-archetype,app-shell-piece. Useful for surfacing related components during discovery.status: covered in its own section below. Get this right.component: the path to the existing file, if one exists. This is the contract pointer. The spec describes the interface; the file is the implementation.
The nine canonical sections
Every spec follows the same nine sections, in this order.
Purpose
One sentence. What is this component for, and why does it exist.
Example:
## Purpose
A two-column hero with an image on the right, used for top-of-page
intros on feature and landing pages.This section serves outcome one: decide whether to use this component vs an alternative. If an AI is choosing between hero-split and hero-centered, the purpose line makes the decision obvious. Generic purpose lines like "A flexible hero component" don't help. They just consume tokens.
Anatomy
A bullet list of visible parts, outer to inner. Each bullet names the part and its canonical class or token.
Example:
## Anatomy
- Wrapper: `section.relative.bg-white` with `overflow-hidden`
- Grid: `grid lg:grid-cols-2 gap-12 items-center`
- Copy block: left column, `max-w-lg`
- Eyebrow: `text-sm font-semibold tracking-wide text-brand-600`
- Heading: `text-4xl font-bold text-gray-900`
- Body: `text-lg text-gray-600`
- Image block: right column, `aspect-video rounded-2xl overflow-hidden`We tried writing anatomy in prose paragraphs on one project. Claude parsed it poorly, missed token references, and generated markup that ignored the grid structure entirely. Bullets are faster and more reliable. We reverted and stayed reverted.
Props
A markdown table: Name / Type / Default / Description. Use TypeScript-style union types.
Example:
## Props
| Name | Type | Default | Description |
|----------|---------------------------------------------------|-----------|--------------------------------------|
| color | `blue`, `purple`, `red`, `gray` | `blue` | Theme for gradient, eyebrow, icons. |
| eyebrow | `string` | `''` | Small label above the heading. |
| heading | `string` | required | Main headline. |
| imageUrl | `string` | required | Path to the right-column image. |This section serves outcome two: pass the right props. The types column earns its width. Union types tell AI exactly which values are blessed. string alone invites hallucinated values.
Usage
One code block, most common use, in the framework's actual syntax.
Example:
<HeroSplit
color="blue"
eyebrow="Introducing"
heading="Design systems that ship"
imageUrl="/images/hero-product.jpg"
/>Outcome two again, with outcome three baked in: AI mimics this pattern when generating code. Show an antipattern in the usage example, and AI reproduces it everywhere.
Variants observed in the site
A table listing real variations, their distinguishing props, and where they appear.
Example:
## Variants observed in the site
| Variant | Props | Where used |
|------------|---------------------------|-------------------------|
| Blue hero | `color="blue"` | Homepage, pricing page |
| Gray hero | `color="gray"` | About, contact |
| No eyebrow | `eyebrow=""` or omitted | Campaign landing pages |This section answers the question AI otherwise guesses at: which variants are actually used, and which are technically possible but wrong for this site. These are the blessed variants. The rest are invitations to drift.
Tokens used
A list of CSS variables and Tailwind utilities the component consumes.
Example:
## Tokens used
- `--color-brand-600` (eyebrow text)
- `--color-gray-900` (heading)
- `bg-gradient-to-br` from `from-brand-50 to-white`
- `rounded-2xl`, `shadow-lg` (image block)This section exists so AI understands the dependency surface before modifying anything. It doesn't need to open the file to know which tokens are in play.
Voice
How copy inside this component should read. Specific to this component, not the general brand.
Example:
## Voice
- Heading: sentence case, period optional, under 10 words preferred.
- Eyebrow: title case, no punctuation, two or three words.
- Body: one to two sentences, plain language, no jargon, period at end.
- No exclamation marks anywhere in this component.Outcome four. This section is the most commonly skipped and the most consequential skip. AI defaults to its own copy conventions. Without a voice section, you get "Discover the future of X" in every headline.
Don't
Three to five imperatives. Common mistakes. State them bluntly.
Example:
## Don't
- Don't change the 4-cell icon count without a refactor. The grid is hardcoded `lg:grid-cols-4`.
- Don't override the `-mt-4` on the top wrapper. It's intentional. It bridges to the preceding section.
- Don't use this component at the top of a page. It assumes a hero or split section came before it.
- Don't add a sixth color theme without updating the `color` prop union type and the token map.We skipped this section on early versions of the format. Felt negative. Felt like distrust. Then Claude rebuilt a container on a sister project with a 5-column icon grid because nothing told it the 4-column structure was load-bearing. We added "Don't" back and never removed it again.
Related
Wiki-style links using [[spec-name]] syntax.
Example:
## Related
- [[hero-centered]] – use when no image is needed
- [[section-row-icons]] – the icon grid that often follows this component
- [[page-shell-marketing]] – the shell this component lives insideThis is how AI traverses the spec graph without loading every spec at once. Each [[link]] is an on-demand pointer. A fresh session can follow the chain as far as the task requires.
Status values
The status field in the frontmatter is not decoration. It changes what AI does with the spec.
documented: the spec describes an existing file. The file atcomponent:is the implementation. AI should import it, not rewrite it.pattern: the recipe appears inline in multiple places but has no canonical file. AI should write it inline using the spec as the recipe.proposed: recommended but not built. If AI is building a new page, it can follow this spec as the intended contract. Building the file is a separate, future task.new: created alongside the spec during the current scaffold pass. Spec and file were written together.
Our first pass at status had two values: done and todo. Claude kept trying to import files that didn't exist. todo didn't tell it whether a file might exist somewhere and just needed finding. The four-value system resolves the ambiguity cleanly. documented means "import this." pattern means "use this recipe inline." proposed means "if building, follow this contract." new means "added with the design system."
Good spec vs vague spec
This is the article's center of gravity. Here's the same component spec written two ways.
Vague (don't write this):
## Purpose
A flexible grid component for displaying content.
## Props
| items | array | required | The items to display |
## Don't
- Don't misuse this component.Specific (write this):
## Purpose
A bordered, rounded container with a header block on top and a 4-column
icon grid below. The canonical row for "Built for X, Y, Z, and A" content.
## Props
| color | `'blue' \| 'green' \| 'purple' \| 'red' \| 'gray' \| 'yellow'` | `'blue'` | Color theme for gradient, eyebrow, icon pads, and accent text. |
## Don't
- Don't change the 4-cell count without a refactor.
The grid is hardcoded `lg:grid-cols-4`.
- Don't override the `-mt-4`. It's intentional. It bridges to the
preceding section.
- Don't use this standalone at the top of a page. It assumes a hero
or split came before it.The specific version is about twice as long. It's infinitely more useful to a fresh session. The vague version occupies context without earning it. It tells AI: "a grid exists." The specific version tells AI: "here's the exact grid, here's what it's for, here's what you cannot change." Those are different things entirely.
Vague specs are a kind of empty politeness. They avoid the work of articulating constraints because articulating constraints feels prescriptive. But prescription is the point. A spec that doesn't prescribe is a placeholder with delusions of documentation.
“Vague specs take up the context window without earning their place.
Length and depth calibration
Target: 2 to 5 KB per spec file.
We built 8 KB specs on a recent client project. Felt thorough. Tested them with Claude. The long specs made the AI worse. Too much context, conflicting signals, decision paralysis. Trimmed to 2 to 4 KB. Better immediately.
The calibration rule we use now:
- Atoms and molecules backed by an existing file: 1.5 to 2 KB. The file is the implementation; the spec covers the interface and the constraints.
- Organisms with multiple variants: 3 to 5 KB. More decisions to capture.
- Anything creeping past 5 KB: the component is probably doing too much. Consider splitting before writing more spec.
“2 to 5 KB. Less is vague. More is unread.
What not to put in a spec
Discipline matters here as much as completeness.
- The implementation source code. Point at the file with
component:. Don't paste the<template>block. The spec is the contract; the file is the implementation. They're separate artifacts for a reason. - Inline TypeScript types. Those live in the component file. The props table is descriptive, not authoritative. If they diverge, the file wins.
- Brand essence and philosophy. That belongs in
DESIGN.mdunder the Overview section. - The full color palette. Also
DESIGN.md. The spec lists only the tokens this component actually uses. - Marketing copy. Even when a component is for marketing, the spec describes the contract. It doesn't write the message.
The rule is simple. If something belongs in a shared reference file, it belongs there. The spec points at those files when needed. It doesn't absorb them.
“Documentation, not regeneration.
The four things, restated
Every section of a well-written spec earns its place by serving one of the original four outcomes.
Purpose and Related help AI decide whether to use this component. Props, Anatomy, Variants, and Usage help AI use it correctly. Don't and Tokens help AI avoid the documented mistakes. Voice helps AI write copy that fits.
This is what we ship in app/design-system/specs/ on every project now. The format took several projects to stabilize. Each project contributed a failure the format wasn't yet equipped to prevent. That's the actual origin of each section. Not design intuition. Observed mistakes, captured as constraints, tested with real sessions until they stopped happening.
A good spec gives a fresh session exactly what it needs and nothing it doesn't. That's the standard. Every section, every field, every Don't entry either earns its place against one of those four outcomes, or it gets cut.

