# AGENTS.md Open Components is a standard for UI components, with guidelines for their user experience (UX), developer experience (DX) and agentic experience (AX). This repository holds the standard, as markdown in `content/docs/`, and the static Nuxt site that publishes it at https://opencomponents.dev, along with the MCP server at https://mcp.opencomponents.dev/mcp (`mcp/`) and the agent plugin (`plugins/open-components/`). Most changes here are to the guidelines themselves, so how they read matters as much as what they say. [Writing the docs](#writing-the-docs) covers the voice, style and format. [`.github/CONTRIBUTING.md`](./.github/CONTRIBUTING.md) explains how every part of the site works, so read the section on the part you're changing. ## Commands ```bash pnpm dev # the site, on http://localhost:3000 pnpm dev:mcp # the MCP server (mcp/port.mjs picks its port) pnpm check-types # vue-tsc on the site, the MCP server and the plugin pnpm check-reference # the component pages show app/reference/ as it is pnpm test # Vitest: reference tests, contracts, MCP server, agent plugin pnpm build # nuxt generate → dist pnpm build:mcp # nuxt build mcp → mcp/.output pnpm check-mcp # after both builds: every tool, resource and prompt against dist pnpm lighthouse # after pnpm build: fails below 100 in any category ``` Building needs Node 22.13 or later. Which checks to run depends on what you changed: | You changed | Run | | ------------------------------------------------ | ---------------------------------------------------------- | | Anything | `pnpm check-types` and `pnpm test` | | A docs page or `app/reference/` | `pnpm check-reference` too | | `mcp/`, or the headings and contracts it reads | `pnpm build`, `pnpm build:mcp`, then `pnpm check-mcp` | | The homepage | `pnpm build`, then `pnpm lighthouse` | ## Where things live | Path | What it holds | | -------------------------------------------- | -------------------------------------------------------------------------------- | | `content/docs/` | The standard. Numbered files and folders set the sidebar order | | `content/changelog/` | One entry per release of something readers will notice | | `app/reference//` | The tested Vue 3 reference implementations, which every page shows as code | | `app/components/content//` | The live examples, anatomy and playground, which render the reference | | `app/data/frameworks.ts` | The frameworks, their order and their `::framework-switcher` slot names | | `server/lib/contract.ts`, `public/schemas/contract.json` | How a page's contract is built, and the JSON Schema it follows | | `mcp/` | The MCP server, a Nuxt app and Cloudflare Worker of its own | | `plugins/open-components/` | The agent plugin: its manifest, `mcp.json` and one skill per MCP prompt | | `app/pages/index.vue` | The homepage, built on the `@uxfront/*` kit | ## Things that change together Most of the site is generated from the docs, so a change in one place often needs a change in another. Where a check catches the drift, it's named here. - **Reference implementations and their pages.** Every file in `app/reference//` is shown in a code block titled with its name, like ```` ```vue [Button.vue] ````, under the page's `## Reference implementation`. Change both, and `pnpm check-reference` confirms they match. - **Checklists.** Rules are rows of the tables under `### UI rules`, `### UX rules`, `### DX rules` and `### AX rules` (on a split page, `## UI Checklist` and so on, on each layer's page after its sections and before its FAQ: the join turns them back into `### UI rules`), with the columns Rule, Level, Scope, Requirement, Basis and Check, named exactly that. Every rule ID is unique, every rule has a Basis and every Check is one `mcp/lib/rules.ts` knows, or `pnpm test` fails. - **Contracts.** A contract is the YAML block under a page's `### Described`. When it gains a field, add it to `public/schemas/contract.json` in the same change, since `pnpm test` validates every contract against it. - **Prompts.** The prompts on the pages (the Button's `### Prompts` on its AX page, the Design Tokens' `### Prompts`), the MCP prompts in `mcp/lib/prompts.ts` and the plugin's skills ask for the same things. `prompts.test.ts` checks the first two, so change all three together. - **Skills.** The plugin's skills name tools, pages, section headings, frameworks and rule IDs, and `pnpm test` checks that they all exist. Renaming a heading a skill reads, like "Developer Experience (DX)", fails it. - **The plugin's version.** Clients cache the plugin by its `version`, so bump it in `plugins/open-components/plugin.json` and in `.claude-plugin/marketplace.json` whenever the plugin changes. - **MCP tools and prompts.** A new tool is named in the instructions in `mcp/nuxt.config.ts`, in `TOOLS` in `scripts/check-mcp.mjs` and on the MCP Server page. A new prompt goes in `PROMPTS` and `filled` there, and gets a skill in the plugin. - **A component that ships.** Its Roadmap row becomes `:roadmap-check{shipped}`, and it gets a changelog entry. - **Replaced theme components.** `app/components/app/AppHeaderBody.vue`, `app/components/docs/DocsPageHeaderLinks.vue` and `app/components/docs/DocsAsideLeftBody.vue` replace the layer's and Docus's own, `app/components/docs/DocsNavigation.vue` renders the sidebar's and the menu's navigation in place of Nuxt UI's `UContentNavigation`, and `app/components/UContentSurround.vue` replaces Nuxt UI's links to the previous and next pages, so both list the component pages by name (`app/lib/navigation.ts`). Keep them in step with the originals. ## Markdown conventions The MCP server reads the same markdown as the site, and knows less MDC syntax than Docus does. Content it can't read doesn't fail the build, but `pnpm test` fails on most of it, so keep to these: - Every page has a `title` and a `description`, and a file name that's unique across `content/docs/` once its number is dropped. The parts of a split page, below, count as one page. - Number the folders and the pages outside `3.components/` to order them in the sidebar, and leave component folders without one, like `3.components/card/`, so the sidebar lists them by name. It sorts file names as text, which would put a `10.` before a `2.`, and two component branches open at once would reach for the same number. - Every component page is split into parts, like the Button's in `3.components/button/`, which stays the model: its Overview in `index.md`, with link cards to the parts, and a page per layer, numbered to set their order inside the folder. A part's `title` is the heading it has on the whole page, its `before` names the Overview's section it goes before, and its headings sit a level higher. After its sections come its rules table, under `## UI Checklist` and so on, then its questions, if it has any, under `## FAQ`. Agents still read one page, which `server/lib/raw.ts` joins back together with one Checklist and one FAQ, and links from before the split go on to the right part (`app/middleware/split-pages.global.ts`). [The docs](./.github/CONTRIBUTING.md#the-docs) has the details. - Headings are plain text and inline code, with no links, emphasis, HTML, components or `{#id}`. - Use block components, `:roadmap-check` and `{external=""}` on links, and nothing else inline, like `:badge[New]`. - Link to generated files, like `/raw/…` and `/llms.txt`, with `{external=""}`, or the router shows the 404 page. - Link within the docs by absolute path, even on the same page, as in `/docs/components/button#checklist`. - Put headings above a `::framework-switcher`, never inside it. Inside a live example, nest the switcher with the same number of colons as the example (`::`), or its slots go to the example. - Avoid Nuxt UI callouts like `::tip` and `::note`, whose text fails contrast in light mode. ## Code conventions - Name CSS variables as token paths, with a double dash between groups and single dashes inside a name: `--color--primary-contrast`, `--button--icon--size`. A part or a state always gets its own group, while CSS property names stay whole, as in `--button--font-size`. The [Design Tokens](./content/docs/2.foundations/1.design-tokens.md) page is the source of truth. - Reference components put their styles in `@layer components`, after `@layer theme, base, components, utilities;`, so a plain class can override their variables. Examples never use inline styles: they set variables from a class. - Reference implementations explain the why in their comments, like the props' doc comments in `Button.vue`. Tests use Testing Library, find elements by role and name, and are named as sentences, like `"is a button, named by its label, that never submits by accident"`. - The MCP server's logic lives in `mcp/lib/` as pure functions, so Vitest can test it. Tools are read-only and report bad input with `createError()`, and the server stays stateless. - The homepage scores 100 in every Lighthouse category. Don't add render-blocking resources, and load heavy code with dynamic `import()`. ## Writing the docs The [Button](./content/docs/3.components/button/index.md) page, split into an Overview and a page per part in `3.components/button/`, is the model for every page, so read the sections you're about to mirror before you write. It's long, about 35,000 tokens, so read it a section at a time. ### Voice Write the way a senior colleague would walk you through it: warm, plain and sure of the facts, never a spec or a sales pitch. - Talk to the reader as "you", and about the project as "we" and "our". - Explain why, every time. A recommendation comes with its reason, joined by "so", "since", "because" or "which": > Hover styles only apply to pointers that can actually hover (`@media (hover: hover)`), otherwise a tap would leave the button looking hovered. - Ground it in what people experience. Screen readers announce, voice control users say "click Save", and keyboard users lose their place. Open a page with what the component does for people, then "Think of…" and three concrete ways it goes wrong. - Outside the checklist, recommend rather than command: "We recommend…", "Try to keep…", "It's best to…", "Keep in mind that…", "You'll want to…". - Keep the standard apart from our implementation. Rules live in the checklist, and the values we chose, like a 2px focus offset, are "what we recommend". The reference implementation "does its part of every rule", but never "meets every rule", since it can't meet usage rules on its own. - Keep the prose framework-agnostic. No `defineProps`, `v-if` or `$el`, and events "fire" rather than "emit". Only mention Vue where the reference implementation's code is shown. - Say things plainly. Sentences that sound wise but say little read as machine-written, so cut them: | Instead of | Write | | -------------------------------------------------------------- | ------------------------------------------------------------------------ | | In a way, semantics are the agent's API. | A real button has a role, a name and states, so an agent can find it. | | `Input.test.ts` holds its tests. | The tests are in `Input.test.ts`. | | …along with a contract they can hold the code to. | …and on a contract they can check the code against. | | Every state is designed. | Each state has a look of its own. | - Only make claims that are true, and give the number and its source: "at least 24 × 24px ([WCAG 2.5.8](…))", not "large enough". ### Style - Use British spelling in prose, like colour, behaviour, recognise and summarise. Code keeps its own: `color`, `forced-colors`. - Leave out the Oxford comma: "React, Vue, Svelte, Angular, Solid, Astro and Vanilla". - Use contractions, like it's, don't, you'll and we'd. - Don't use em dashes, semicolons or exclamation marks in prose. A comma, a colon or a new sentence does the job. The one exception is the `**Part** — description` items in an Anatomy list. - Write "like", "such as" or "for example" rather than "e.g.", "i.e." or "etc.", and leave out filler like "simply", "easily", "seamless", "robust", "leverage" and "ensure". - Write "the Button" for our component and its page, and "a button" for buttons in general. - Use code formatting for props and values (`variant="link"`), elements (`