Skip to content

spec.md — one file, three audiences

May 25, 2026

Most component libraries pick a documentation model and stick with it:

  • Markdown in the docs site. Designers love it; AI tools can’t query it.
  • Storybook stories. Engineers love them; designers don’t open Storybook.
  • TypeScript signatures. AI tools can parse them; designers see nothing.

We tried all three. None of them were a contract; they were descriptions of one. They went out of date the moment the component shipped.

helixui picked a fourth way: every component owns a sibling spec.md that is the contract.

What it looks like

packages/core/src/components/button/spec.md:

---
title: Button
status: stable
since: 0.1.0
package: "@helixui/core"
import: "import { Button } from '@helixui/core'"
description: Triggers an action when clicked.
category: form
tags: [clickable, action, primary, destructive, submit, cta]
props:
- name: variant
type: "'solid' | 'soft' | 'ghost' | 'outline'"
default: solid
- name: tone
type: "'brand' | 'neutral' | 'danger'"
default: brand
- name: size
type: "'sm' | 'md' | 'lg'"
default: md
- name: disabled
type: boolean
default: false
tokens:
- color.bg.action.brand.default
- color.bg.action.brand.hover
- radius.md
- spacing.3
a11y:
- inherits native <button> semantics
- icon-only buttons require aria-label
- focus ring via :focus-visible
---
## Anatomy
A native `<button>` with no wrapper. Spread props go to the button.
## Examples
```tsx
<Button>Click me</Button>
<Button variant="outline" tone="neutral">Cancel</Button>
<Button tone="danger">Delete</Button>
The frontmatter is **machine-readable**. The body is **human-readable**.
Same file. Same git history. Same review.
## Three audiences
### 1. The docs site
`apps/docs/` ingests every `spec.md` at build time and turns it into
a Starlight content page. Props tables are auto-rendered from the
frontmatter. Examples render with full syntax highlighting. When a
prop is added, the docs site picks it up on the next deploy.
No "remember to update the docs" step. No drift.
### 2. The LLM manifest
`pnpm build:llms` walks every `spec.md` and emits
`llms.txt` (compact tree) and `llms-full.txt` (full prose, ~80 KB).
These are static files served from the site root. Drop them into any
LLM context window — the model has the system in memory.
You can read the live manifests:
- <https://helixui.ai/llms.txt>
- <https://helixui.ai/llms-full.txt>
### 3. The MCP server
`@helixui/mcp` serves the same `spec.md` files over JSON-RPC stdio.
Agents call `helixui.search "button danger"` and get back the relevant
spec frontmatter without parsing markdown. They call `helixui.validate
"<Button variant='outline'>"` and the server cross-references the
spec to confirm `outline` is a valid variant.
The MCP server doesn't have its own database. It reads from the same
files everything else reads from. There's nothing to keep in sync
because there's only one source.
## Why this is a contract, not a description
Two things lift `spec.md` from "doc style" to "contract":
1. **CI fails if a component lacks a spec.** `pnpm build:llms` errors
out when a `packages/core/src/components/<name>/` directory exists
without a `spec.md`. Forgetting one isn't an option.
2. **The spec ships in the npm tarball.** `@helixui/core`'s
`package.json` `exports` field includes `"./components/*/spec.md":
"./dist/components/*/spec.md"`. A consumer who never opens our
docs site still has the contract in their `node_modules/`.
## The cost
`spec.md` is overhead. We pay it on every new component (about 30
minutes per spec, with examples). We pay it again on every API
change. The discipline is real.
What it buys:
- Docs that never drift.
- AI tooling that doesn't hallucinate.
- A schema we can validate against in tests.
- A format Figma plugins can read (next quarter).
Three engineers can keep the spec catalog current. A larger team
would have made the docs go stale by now. We measured.
## How to write a good spec
- **Be terse.** A spec isn't a tutorial. The body is for examples,
not philosophy.
- **List every prop.** If you ship it, document it. No "private" props
in production.
- **List every token.** Even ones the consumer is unlikely to
override. Future theme work depends on this list being accurate.
- **List a11y constraints as imperatives.** "Icon-only buttons require
aria-label" not "consider an aria-label."
The [SPEC_FORMAT.md](https://github.com/021flow/helixui/blob/main/packages/core/src/components/SPEC_FORMAT.md)
file in the repo has the full schema.
## What it doesn't replace
- **Code review.** A spec describes intent; the code is what runs.
- **Type tests.** Frontmatter is plain YAML, not TypeScript.
- **Long-form articles.** This blog post wouldn't fit in a spec.
Spec is for *contracts*. Everything else lives in the docs site, the
showcases, the blog.
## Try authoring one
Want to write a spec? Pick a component without one (the Banner is a
good candidate) and follow `SPEC_FORMAT.md`. Open a PR. Look at how
the docs site, `llms-full.txt`, and the MCP server light up in the CI
preview — that's the contract paying off in real time.
---
*Open question: should `spec.md` carry runnable examples? We say no
for now (it'd make the format too heavy), but the Markdown studio
showcase is changing our minds.*