# Introduction Open Components is the UXFront component standard: guidelines for building UI components with a perfect user experience, developer experience and agentic experience, whether humans or AI agents write them. Learn the guidelines once, then hold every component to the same high standards, whether you or your agents write it. ## The three layers The standard has three layers, one for each audience a component serves. A component meets the standard when it meets all three. ### User experience Components behave the way people expect them to, with any input, on any device and for every ability. - **Accessible**: keyboard, pointer, touch and screen readers, following [WAI-ARIA](https://www.w3.org/WAI/ARIA/apg/){rel=""nofollow""}. - **Predictable**: the same interaction works the same way in every component. - **Every state**: hover, focus, active, disabled, loading and error, all designed. - **Adaptive**: respects motion, contrast, colour scheme and text size preferences. ### Developer experience One clear API, learned once and used everywhere. Knowing one component means knowing them all. - **Consistent**: sizes, colours, states and events share their names everywhere. - **Type-safe**: every prop, event and slot is typed and documented. - **Composable**: small parts build larger ones, with no hidden coupling. - **Controllable**: controlled or uncontrolled, with every state within reach. ### Agentic experience Components that AI agents can read, reason about and build with, so their output meets the same bar as yours. - **Semantic**: roles, names and states agents can read straight from the markup. - **Described**: anatomy, props and states, published as machine-readable specs. - **Deterministic**: the same input always renders the same structure. - **Verifiable**: rules agents can check their work against before shipping. ## Foundations Some rules hold for every component rather than for one. [Design Tokens](https://opencomponents.dev/docs/foundations/design-tokens) covers how to name the CSS variables behind them, a convention we call token paths, and the theme tokens they all share. ## Components Each component page holds one component to the three layers, and also covers how it looks, which we call its UI. Every page follows the same structure, so you and your agents always know where to look: - **At a glance**: a single table with its element, role, keyboard support and options. - **UI**: its anatomy, variants, colours, sizes, states and tokens. - **UX**, **DX** and **AX**: the three layers, one trait at a time. - **Checklist**: every rule, with a stable ID like `button/keep-focus`, a level, a scope that says whether the component or the code using it meets the rule, a basis that says where it comes from, like a WCAG success criterion or our own call, and a way to check it. - **Reference implementation**: a component that does its part of every rule, along with its tests. The code is in Vue for now. The [Button](https://opencomponents.dev/docs/components/button) is a great place to start, and the [Roadmap](https://opencomponents.dev/docs/getting-started/roadmap) lists the components we plan to ship next. ## For agents If your agent supports MCP, connect it to our [MCP server](https://opencomponents.dev/docs/getting-started/mcp-server) at `https://mcp.opencomponents.dev/mcp`. It gives your agent the contracts, every rule and the docs as tools, so it reads a component's contract first, then only the sections it needs rather than the whole page. Our [agent plugin](https://opencomponents.dev/docs/getting-started/agent-plugin) connects the server for you, along with skills that build and review components the way the standard asks, so your agent follows it without being told how. It works in VS Code, GitHub Copilot, Cursor, Codex, Claude Code and other clients that support [Agent Plugins](https://agent-plugins.org){rel=""nofollow""}. Every page of these docs is also published as markdown: prefix its path with `/raw` and add `.md`, as in [`/raw/docs/getting-started/introduction.md`](https://opencomponents.dev/raw/docs/getting-started/introduction.md){external=""}. [`/llms.txt`](https://opencomponents.dev/llms.txt){external=""} lists every page, and [`/llms-full.txt`](https://opencomponents.dev/llms-full.txt){external=""} holds them all in one file. Pages that describe a contract, like the Button's, also publish it on its own, with every rule from their checklist: swap `.md` for `.yaml`, as in [`/raw/docs/components/button.yaml`](https://opencomponents.dev/raw/docs/components/button.yaml){external=""}. It's a fraction of the page's length, for when an agent only needs the rules, and not the reasoning behind them. Every contract follows the same [JSON Schema](https://opencomponents.dev/schemas/contract.json){external=""}, so you and your agents can validate one, including a contract you write for a component of your own. Each contract names the schema on its first line, so editors that use the YAML language server, like VS Code with its YAML extension, check it as you type. # Roadmap Open Components grows one component at a time. We'd love to cover every component you reach for, but a page only helps you if you can trust it, so we write each one in full before we move on to the next. This page shows you where we are, and what's coming next. Every component gets a page with the same structure as the [Button](https://opencomponents.dev/docs/components/button)'s, so you always know where to look: - **User Interface** (UI) - how it looks, from its anatomy to its tokens - **User Experience** (UX) - how it behaves for every person and every input - **Developer Experience** (DX) - how developers work with it - **Agentic Experience** (AX) - how agents can read and verify it On top of that, each page comes with a checklist of every rule, a contract your agents can check their work against, and a reference implementation that does its part of every rule, along with the tests that prove it. That's a lot to get right, so a page only ships once all of it is ready. We'd much rather give you one page you can rely on than several you can't. ## Components Here's every component we plan to cover. A tick means its page has shipped, so you can read it today. We've grouped them by what they build on, so each group can reuse what the ones before it have already settled, like the Button's sizes and colours, or how the Dialog handles focus. The order may still change as we go, and you can help shape it. ### Actions and forms Let's start with the components people use to act and to fill in forms. The Button sets the sizes, colours and states that every other component shares, and the form controls build on them with the labels, hints, validation and controlled values that every form needs. | Component | What it's for | | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | [Button](https://opencomponents.dev/docs/components/button) | Acting on what's in front of you, from saving a form to deleting a file | | Field | Labelling a form control, with a hint and an error message when it needs them | | [Input](https://opencomponents.dev/docs/components/input) | Typing a single line of text, like a name, an email address or a search | | [Textarea](https://opencomponents.dev/docs/components/textarea) | Writing longer text, like a comment or a message | | [Checkbox](https://opencomponents.dev/docs/components/checkbox) | Saying yes or no to one thing, like agreeing to the terms | | [Checkbox Group](https://opencomponents.dev/docs/components/checkbox-group) | Picking any number of options out of a few | | [Radio Group](https://opencomponents.dev/docs/components/radio-group) | Picking one option out of a few, when they all fit on the screen | | [Switch](https://opencomponents.dev/docs/components/switch) | Flipping a setting on or off, with an effect you see right away | | Select | Picking one option out of many, from a list | ### Overlays Next come the components that sit on top of the page. They all need to take focus and give it back, close with `Escape` or a click outside, and stack in the right order, so we'll settle those rules once and share them. | Component | What it's for | | -------------------------------------------------------------- | ----------------------------------------------------------------------------- | | [Dialog](https://opencomponents.dev/docs/components/dialog) | Asking for someone's full attention, and holding focus until they're done | | [Popover](https://opencomponents.dev/docs/components/popover) | Showing more next to what opened it, without blocking the rest of the page | | [Tooltip](https://opencomponents.dev/docs/components/tooltip) | Giving a short hint on hover and focus, like an icon-only button's label | | Dropdown Menu | Tucking a list of actions behind a single button | | Toast | Confirming something briefly without getting in the way, like "Changes saved" | ### Navigation and disclosure These help people find their way around a site, or show and hide what's on a page. Most of them build on the Link and the Button, and add the keyboard support people expect, like moving between tabs with the arrow keys. | Component | What it's for | | ----------- | ------------------------------------------------------------------ | | Link | Taking people somewhere else, from inside a sentence or on its own | | Tabs | Switching between panels of related content, one at a time | | Accordion | Expanding and collapsing sections, like the questions in an FAQ | | Breadcrumb | Showing where a page sits in the site, and the way back up | | Pagination | Moving through a long list of results, a page at a time | ### Feedback and display These tell people what's going on, and most of them aren't interactive at all. That's why their rules are mostly about meaning and contrast, and about making sure screen readers hear about what changes. | Component | What it's for | | -------------------------------------------------------------- | --------------------------------------------------------------------------------- | | [Spinner](https://opencomponents.dev/docs/components/spinner) | Showing that something is busy, for a wait of unknown length | | Alert | Telling people something important, from a success to an error, right on the page | | Badge | Marking a status or a count, like "New" or "3" | | Avatar | Showing a person or a team, with a photo, their initials or an icon | | Progress | Showing how far along a task is, even when you can't tell how long it'll take | | Skeleton | Holding the layout while content loads, so nothing jumps around when it arrives | | [Card](https://opencomponents.dev/docs/components/card) | Grouping related content, like a project or a product, in one container | ### Later Finally, the composite widgets. They have the most keyboard behaviour of all, and they build on almost everything above, which is why they come last. A combobox, for example, is an input, a popover and a list working together. | Component | What it's for | | ---------------- | -------------------------------------------------------------------- | | Combobox | Typing to filter a list of suggestions, like when you pick a country | | Slider | Picking a value or a range by dragging, or with the arrow keys | | Date Picker | Typing a date or a range, or picking it from a calendar | | Table | Comparing rows of data, with sorting and selection | | Tree View | Browsing nested items, like the folders in a file explorer | | Command Palette | Finding any action from the keyboard, just by typing its name | ## Before a page ships You don't have to wait for a page to hold a component to the standard. The three layers in the [Introduction](https://opencomponents.dev/docs/getting-started/introduction) apply to every component, [Design Tokens](https://opencomponents.dev/docs/foundations/design-tokens) covers how to name its variables, and the [Button](https://opencomponents.dev/docs/components/button) shows how the layers turn into rules you can check. Your agents can do the same. Point them at [`/llms-full.txt`](https://opencomponents.dev/llms-full.txt){external=""}, which holds the whole standard in one file, and ask them to follow the Button's structure. For a component that doesn't have its page yet, you could use: ```text Build a Tabs component for our design system that meets the Open Components standard: https://opencomponents.dev/llms-full.txt There's no Tabs page yet, so follow the Button page's structure. Work through the UI, UX, DX and AX of tabs, following the WAI-ARIA Tabs pattern, then write a checklist with a stable ID for every rule, like tabs/arrow-keys, and a test for each one. ``` ## Suggest a component Is a component you need missing, or further down the list than you'd like? [Open an issue](https://github.com/uxfront-com/open-components/issues/new/choose){rel=""nofollow""} and tell us what you're building with it. Knowing what you need helps us decide what comes next. # MCP Server Our MCP server gives your agents the whole standard as tools: the contracts, every rule, the docs and the reference implementations. Connect it once, then ask your agent to build or review a component. It reads only what the task needs, then checks its work rule by rule, citing each rule by its ID. It works with Claude, ChatGPT, Codex, Cursor, VS Code, GitHub Copilot, Gemini CLI and any other client that supports remote servers over the [Model Context Protocol](https://modelcontextprotocol.io){rel=""nofollow""}. It's free, with nothing to install and no account or API key to set up. This page covers: - **Quick start** - connect it and try it in a minute - **Connect your client** - the setup for each client, step by step - **What you can ask** - prompts for building, reviewing and learning the standard - **How it works** - what your agent reads, and in what order - **Tools, prompts and resources** - everything the server offers - **Privacy and security** - what it sees, and what it can't do - **Troubleshooting** - what to check when it doesn't connect ## At a glance | Aspect | MCP server | | -------------- | ------------------------------------------------------------------------------------------------------------- | | URL | `https://mcp.opencomponents.dev/mcp` | | Name | `open-components`, the name to add it under | | Transport | Streamable HTTP | | Authentication | None, so where a client asks, choose none | | Access | Read-only. It serves the standard and changes nothing | | Cost | Free | | Tools | `list-components`, `get-contract`, `list-rules`, `get-page`, `search-docs` and `get-reference-implementation` | | Prompts | `build-component`, `review-component`, `review-usage` and `adopt-token-paths` | | Content | The same files as this site, redeployed whenever they change | ## Quick start ::steps{level="3"} ### Add the server In a coding agent, it takes one command: :::code-group ```bash [Claude Code] claude mcp add --transport http open-components https://mcp.opencomponents.dev/mcp ``` ```bash [Codex] codex mcp add open-components --url https://mcp.opencomponents.dev/mcp ``` ```bash [Gemini CLI] gemini mcp add --scope user --transport http open-components https://mcp.opencomponents.dev/mcp ``` ```bash [Copilot CLI] copilot mcp add --transport http open-components https://mcp.opencomponents.dev/mcp ``` ::: In an editor, open the [Cursor install link](https://mcp.opencomponents.dev/mcp/deeplink){rel=""nofollow""} or the [VS Code install link](https://mcp.opencomponents.dev/mcp/deeplink?ide=vscode){rel=""nofollow""}. On every page of these docs, the menu next to **Copy page** can also copy the server's URL for you, with **Copy MCP Server URL**, or install it in Cursor, with **Add MCP Server**. For any other client, see [Connect your client](https://opencomponents.dev/#connect-your-client). To get skills that build and review components along with the server, install our [agent plugin](https://opencomponents.dev/docs/getting-started/agent-plugin) instead. ### Check that it's connected Run `/mcp` in Claude Code, Codex or Gemini CLI, or `/mcp show open-components` in Copilot CLI, to check that `open-components` is connected. In an editor, look for it in the MCP settings, along with its six tools. ### Ask your agent Ask for what you need in your own words. Mention the server by name the first time, so your agent knows to reach for it: ```text Build a Button component for our React design system that meets the Open Components standard, using the open-components MCP server. Check your work against every rule with a component or both scope, and cite the rule ID for any rule you can't meet. ``` [What you can ask](https://opencomponents.dev/#what-you-can-ask) has more prompts to start from. :: ## Connect your client Every client needs the same three things: the URL, `https://mcp.opencomponents.dev/mcp`, the name, `open-components`, and no authentication. Most take a single command or a few lines of configuration. There's nothing to sign in to, so where a client asks about authentication, choose none. ### Coding agents ::tabs :::tabs-item{label="Claude Code"} ```bash claude mcp add --transport http open-components https://mcp.opencomponents.dev/mcp ``` That adds it for you, in the project you're in. Add `--scope user` to use it in every project, or `--scope project` to share it with your team through the project's `.mcp.json`, which asks each of them to approve it once. Run `/mcp` in Claude Code to check that it's connected. ::: :::tabs-item{label="Codex"} ```bash codex mcp add open-components --url https://mcp.opencomponents.dev/mcp ``` That adds it to `~/.codex/config.toml`, which the Codex CLI, the Codex extension for your editor and Codex in the ChatGPT desktop app all read. To share it with your team, add it to `.codex/config.toml` in your project instead, which Codex reads once you trust the project: ```toml [.codex/config.toml] [mcp_servers.open-components] url = "https://mcp.opencomponents.dev/mcp" ``` Run `/mcp` in Codex to check that it's connected. ::: :::tabs-item{label="Gemini CLI"} ```bash gemini mcp add --scope user --transport http open-components https://mcp.opencomponents.dev/mcp ``` That adds it to `~/.gemini/settings.json`, which Gemini Code Assist's agent mode in VS Code (Standard and Enterprise) reads too, after **Developer: Reload Window**. Leave out `--scope user` to add it to `.gemini/settings.json` in the folder you're in instead, and start `gemini` from that folder, though not from your home folder, where it reports success but saves nothing. Run `/mcp` in Gemini CLI to check that it's connected. ::: :::tabs-item{label="Copilot CLI"} ```bash copilot mcp add --transport http open-components https://mcp.opencomponents.dev/mcp ``` That adds it to `~/.copilot/mcp-config.json`. Run `/mcp show open-components` in a Copilot CLI session to check that it's connected. ::::note On Copilot Business and Enterprise, an organization owner turns on the **MCP servers in Copilot** policy first. :::: ::: :::tabs-item{label="Grok Build"} In Grok Build, xAI's coding agent: ```bash grok mcp add --transport http open-components https://mcp.opencomponents.dev/mcp ``` That adds it to `~/.grok/config.toml`. Run `grok mcp doctor open-components` to check that it's connected. ::: :::tabs-item{label="Hermes Agent"} ```bash hermes mcp add open-components --url https://mcp.opencomponents.dev/mcp ``` Answer `n` when it asks whether the server needs authentication, then let it enable every tool. That adds it to `~/.hermes/config.yaml` (`%LOCALAPPDATA%\hermes\config.yaml` on Windows), which you can also edit yourself: ```yaml [~/.hermes/config.yaml] mcp_servers: open-components: url: "https://mcp.opencomponents.dev/mcp" ``` Run `hermes mcp test open-components` to check that it's connected. In the Hermes desktop app, add it under **Capabilities > Connectors > Add your own**: set **Type** to **Streamable HTTP**, leave **Auth** on **None** and click **Save**. ::: :: ### Editors ::tabs :::tabs-item{label="Cursor"} Open the [Cursor install link](https://mcp.opencomponents.dev/mcp/deeplink){rel=""nofollow""}, or add the server to `.cursor/mcp.json` in your project, or to `~/.cursor/mcp.json` to use it in every project: ```json [.cursor/mcp.json] { "mcpServers": { "open-components": { "url": "https://mcp.opencomponents.dev/mcp" } } } ``` ::: :::tabs-item{label="VS Code"} Open the [VS Code install link](https://mcp.opencomponents.dev/mcp/deeplink?ide=vscode){rel=""nofollow""}, or add the server to a `.mcp.json` at your project's root, which Claude Code reads too: ```json [.mcp.json] { "mcpServers": { "open-components": { "type": "http", "url": "https://mcp.opencomponents.dev/mcp" } } } ``` In `.vscode/mcp.json`, the same entry goes under `servers` rather than `mcpServers`. Copilot Chat can then use its tools in agent mode. ::::note On Copilot Business and Enterprise, an organization owner turns on the **MCP servers in Copilot** policy first. :::: ::: :::tabs-item{label="Visual Studio"} Add the server to `.mcp.json` in your solution, or to `%USERPROFILE%\.mcp.json` to use it in every solution: ```json [.mcp.json] { "servers": { "open-components": { "type": "http", "url": "https://mcp.opencomponents.dev/mcp" } } } ``` Then turn on its tools from **Tools** in Copilot Chat's agent mode. ::::note On Copilot Business and Enterprise, an organization owner turns on the **MCP servers in Copilot** policy first. :::: ::: :::tabs-item{label="JetBrains"} In AI Assistant 2025.3 or later, go to **Settings > Tools > AI Assistant > Model Context Protocol (MCP)**, click **Add**, choose **HTTP** and paste: ```json { "mcpServers": { "open-components": { "url": "https://mcp.opencomponents.dev/mcp" } } } ``` Click **OK**, then **Apply**. To let Junie and the other agents in your IDE use it too, turn on **Pass custom MCP servers** under **Settings > Tools > AI Assistant > Agents**. For Junie CLI, add the same entry to `~/.junie/mcp/mcp.json`, or to `.junie/mcp/mcp.json` in your project. ::: :::tabs-item{label="Zed"} Add it under **Settings > AI > MCP Servers > Add Server > Add Remote Server** (in older releases, in the Agent Panel's settings), or to `context_servers` in your `settings.json`: ```json [settings.json] { "context_servers": { "open-components": { "url": "https://mcp.opencomponents.dev/mcp" } } } ``` The dot next to it turns green once it's connected. ::: :::tabs-item{label="Antigravity"} ```bash agy mcp add open-components https://mcp.opencomponents.dev/mcp ``` That adds it to `~/.gemini/config/mcp_config.json`, which the Antigravity CLI, the IDE and the desktop app share. In the IDE, open it from the agent panel's **…** menu, under **MCP Servers > Manage MCP Servers > View raw config**. Give a remote server a `serverUrl` there, the key Antigravity's docs ask for: ```json [~/.gemini/config/mcp_config.json] { "mcpServers": { "open-components": { "serverUrl": "https://mcp.opencomponents.dev/mcp" } } } ``` ::: :::tabs-item{label="Windsurf and Devin"} In Windsurf, add the server to `~/.codeium/windsurf/mcp_config.json`: ```json [~/.codeium/windsurf/mcp_config.json] { "mcpServers": { "open-components": { "serverUrl": "https://mcp.opencomponents.dev/mcp" } } } ``` With the Devin CLI: ```bash devin mcp add -s user open-components https://mcp.opencomponents.dev/mcp ``` ::: :: ### Chat apps ::tabs :::tabs-item{label="Claude"} In Claude and Claude Desktop, add the server as a custom connector: 1. Go to **Customize > Connectors**, click **+ Add**, then **Add custom connector**. 2. Name it `open-components` and enter the URL. 3. Under **Authentication**, choose **No sign in**. You can then turn it on for a conversation from the **+** button, under **Connectors**. ::::note On Team and Enterprise plans, an owner adds it for everyone first, under **Organization settings > Connectors** . :::: ::: :::tabs-item{label="ChatGPT"} You can add it on the web, with a Plus, Pro, Business, Enterprise or Edu plan: 1. Turn on **Developer mode** in ChatGPT's settings. 2. Create an app named `open-components` with the URL, set **Authentication** to **No Authentication** and confirm that you trust the server. 3. In a new chat, pick it from the **+** menu, under **Developer mode**. The menus can differ a little from plan to plan. ::::note On Enterprise and Edu workspaces, an admin allows **Developer mode** first. On Business, only admins and owners can use it, so they add the app for everyone. :::: ::: :::tabs-item{label="Grok"} On [grok.com](https://grok.com/connectors){rel=""nofollow""}: 1. Open **Connectors**, choose **New Connector**, then **Custom**. 2. Name it `open-components`, enter the URL as its **Server URL** and click **Add Connector**. ::::note On Grok Business and Enterprise, an admin adds it for everyone first, in the xAI console under **Grok Business > Connectors** . :::: ::: :: ### Cloud agents and APIs ::tabs :::tabs-item{label="Copilot cloud agent"} For Copilot's cloud agent on github.com, a repository admin pastes this under the repository's **Settings > Copilot > MCP servers**, then clicks **Save MCP configuration**: ```json { "mcpServers": { "open-components": { "type": "http", "url": "https://mcp.opencomponents.dev/mcp", "tools": ["*"] } } } ``` ::::note On Copilot Business and Enterprise, an organization owner turns on the **MCP servers in Copilot** and **Copilot cloud agent** policies first. :::: ::: :::tabs-item{label="xAI API"} Pass the server to the Responses API as a tool, and xAI connects to it for you: ```json "tools": [ { "type": "mcp", "server_url": "https://mcp.opencomponents.dev/mcp", "server_label": "open-components" } ] ``` ::: :: ### Other clients - **Amp**: ```bash amp mcp add open-components https://mcp.opencomponents.dev/mcp ``` - **Cline**: open **Customize** (the wrench), then **MCP > Add Remote Server**, and keep **Streamable HTTP** as its transport. - **Continue**: add the server to the `mcpServers` list in `~/.continue/config.yaml`. Continue uses it in Agent and Plan modes. ```yaml mcpServers: - name: open-components type: streamable-http url: https://mcp.opencomponents.dev/mcp ``` - **Crush**: from Crush 0.88, add this line to `~/.config/crush/crushrc`: ```bash mcp add open-components --type http --url https://mcp.opencomponents.dev/mcp ``` - **Factory Droid**: ```bash droid mcp add open-components https://mcp.opencomponents.dev/mcp --type http ``` - **goose**: in the desktop app, go to **Extensions > Add custom extension**, choose **Streamable HTTP** as its type and enter the URL as its endpoint. In the CLI, run `goose configure` and choose **Add Extension > Remote Extension (Streamable HTTP)**. - **LM Studio**: add the server under `mcpServers` in `~/.lmstudio/mcp.json`, which **Program > Install > Edit mcp.json** opens: ```json { "mcpServers": { "open-components": { "url": "https://mcp.opencomponents.dev/mcp" } } } ``` - **OpenCode**: ```bash opencode mcp add open-components --url https://mcp.opencomponents.dev/mcp ``` :brIn releases from before June 2026, run `opencode mcp add` and answer its questions instead. - **Qwen Code**: ```bash qwen mcp add --transport http open-components https://mcp.opencomponents.dev/mcp ``` - **Warp**: under **Settings > Agents > MCP servers**, click **+ Add** and paste: ```json { "open-components": { "url": "https://mcp.opencomponents.dev/mcp" } } ``` Any other client that supports remote MCP servers over streamable HTTP only needs the URL. If yours only runs local servers, it can still reach this one through [mcp-remote](https://www.npmjs.com/package/mcp-remote){rel=""nofollow""}, which you add as a local server with this command: ```bash npx -y mcp-remote https://mcp.opencomponents.dev/mcp ``` ## What you can ask Once the server is connected, ask in your own words. Mention it by name the first time, and your agent takes it from there: the server tells it which tools to use, and in what order. Here's what it can help with, and a prompt to start from for each. ### Build a component ```text Build a Button component for our React design system that meets the Open Components standard, using the open-components MCP server. Check your work against every rule with a component or both scope, and cite the rule ID for any rule you can't meet. ``` Your agent reads the Button's contract with `get-contract`, ports the reference implementation and its tests from `get-reference-implementation`, and reads how the API looks in React with `get-page`. Then it checks its work against the contract's rules with a component or both scope. For a component on the [Roadmap](https://opencomponents.dev/docs/getting-started/roadmap), which has no contract yet, it holds it to the three layers and Design Tokens instead, following the Button's structure. The `build-component` prompt asks for the same. ### Review a component ```text Review our Button, in src/components/Button.tsx, against the Open Components standard, using the open-components MCP server. Report every rule with a component or both scope as pass or fail, with its rule ID and the evidence from our code. Then fix the failures. ``` Your agent lists the rules to report on with `list-rules`, then checks your code against each one. For a rule it's unsure of, it finds the section that explains it with `search-docs`, and reads it with `get-page`. The `review-component` prompt asks for the same. ### Review a screen ```text Review how our checkout page uses buttons against the Open Components standard, using the open-components MCP server. Report every rule with a usage or both scope as pass or fail, with its rule ID and the evidence from our code. ``` Usage rules are the ones the code using a component meets, like `button/one-primary`, which keeps one solid primary button per view. Your agent lists them with `list-rules`, with a usage or both scope. The `review-usage` prompt asks for the same. ### Move to token paths ```text Rename our CSS variables to follow the Open Components token paths, using the open-components MCP server. List our token groups first, then rename every variable along with every var() that reads it, keeping the values as they are. Then add the Stylelint rule from the standard, and fix anything it reports. ``` Your agent reads the [Design Tokens](https://opencomponents.dev/docs/foundations/design-tokens) contract with `get-contract`, and the Stylelint rule from the page's Lint section with `get-page`. The `adopt-token-paths` prompt asks for the same. ### Learn the standard ```text Using the open-components MCP server, how should a button behave while it's loading? Link to the sections you used. ``` Your agent finds the sections that cover it with `search-docs`, then reads them with `get-page`, with the examples in your framework if you name one. To see what the standard covers so far, and what's planned, ask which components it has, and your agent lists them with `list-components`. ## How it works You could point your agent at a page instead, but component pages are long. The Button's is about 35,000 tokens, mostly reasoning and examples in seven frameworks, which is far more than any one task needs. Its contract holds every requirement in about 3,000. When your agent connects, the server tells it how to use it, so you don't have to. Your agent: 1. **Finds what the task is about**, with `list-components`, or `search-docs` for a topic. 2. **Reads the contract first**, with `get-contract`: the component's API, its DOM contract, its tokens and every rule. 3. **Reads only the sections it needs**, with `get-page`, for the reasoning or an example behind a rule, in your framework. 4. **Starts from the reference implementation**, with `get-reference-implementation`, when it builds a component. 5. **Checks its work rule by rule**, with `list-rules`, citing every rule by its ID, like `button/keep-focus`, exactly as the server writes it, so you can look each one up. ### Rules and scopes Every rule has a level. A component meets the standard when it meets every must rule, and each should rule is expected unless there's a good reason not to follow it. A component's rules also have a scope, which says whose code to check them against: | Scope | Met by | Check it when you | | ----------- | ------------------------------------------------- | --------------------------- | | `component` | The component itself | Build or review a component | | `usage` | The code that uses it | Review a screen | | `both` | The two together, like naming an icon-only button | Do either | ### Always in step with the site The server is built from the same files as this site, and redeployed whenever they change, so it serves the standard exactly as the site publishes it. A contract from `get-contract` is the same file as its `/raw/docs/.yaml`, and a page from the resources is the same as its `/raw/docs/.md`. ## Tools, prompts and resources ### Tools Your agent calls these on its own, following the instructions the server gives it. Every tool is read-only, and tells your client so. | Tool | What it returns | Arguments | | ------------------------------ | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `list-components` | What the standard covers, shipped or planned, with the name the other tools take, like `button` | Optionally, `status` | | `get-contract` | The contract of a component or foundation, as YAML: its API, its DOM contract, its tokens and every rule | `component` | | `list-rules` | Rules as records, each with its ID, level, scope, requirement and how to check it, 50 at a time | Optionally, `component`, `ids`, `scope`, `layer`, `level`, `check` or `automated`, and `offset` and `limit` | | `get-page` | A page, or only the sections you name, with the examples in one framework | `path`, and optionally `sections` and `framework` | | `search-docs` | The sections and rules that best match a query, across the whole standard | `query`, and optionally `component`, `type` and `limit` | | `get-reference-implementation` | A component's reference implementation in Vue 3 and its tests, or only the files you name | `component`, and optionally `files` | A page longer than about 8,000 tokens, like the Button's, comes back as its outline, every heading with its anchor and length, so your agent can ask for the sections it needs. `list-rules` returns up to 50 rules at a time, with `total` counting every rule that matches and `nextOffset` when there are more, which your agent passes as `offset` to get the next page. No reply is longer than about 36,000 characters, so clients like Gemini CLI and Codex pass it on whole. ### Prompts Prompts are requests you start yourself, usually as slash commands. They ask for what the prompts on our pages ask for, then point your agent at the tools. | Prompt | What it does | Arguments | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | | `build-component` | Builds a component in your framework, from its contract, reference implementation and tests, or for one on the Roadmap, following the Button's structure | `component`, and optionally `framework` | | `review-component` | Reviews the code of a component or foundation, reporting every rule as pass or fail with its ID and the evidence, then fixes the failures | `component`, and optionally `code` | | `review-usage` | Does the same for a screen that uses a component, against the rules the screen meets | `component`, and optionally `screen` | | `adopt-token-paths` | Renames your CSS variables to [token paths](https://opencomponents.dev/docs/foundations/design-tokens), then adds the Stylelint rule that keeps them that way | None | How you start one depends on your client: | Client | Command | | ----------- | ------------------------------------------------------------------------- | | Claude Code | `/mcp__open-components__build-component button react` | | Gemini CLI | `/build-component button --framework=react` | | VS Code | `/mcp.open-components.build-component`, which then asks for its arguments | Not every client runs them: Codex doesn't, and Continue, OpenCode and Zed can't pass their arguments yet, so only `adopt-token-paths` works there. Where yours doesn't, ask for the same in your own words, as in [What you can ask](https://opencomponents.dev/#what-you-can-ask), or install our [agent plugin](https://opencomponents.dev/docs/getting-started/agent-plugin), whose skills your agent picks up on its own. ### Resources Resources are files you attach to a conversation yourself, with `@` in Claude Code, or **Add Context > MCP Resources** in VS Code. They're every contract and every page, at the same URLs as on this site, `/raw/docs/.yaml` and `.md`, and the [contract schema](https://opencomponents.dev/schemas/contract.json){external=""}. ## Privacy and security The server only serves the standard, so there's little that can go wrong, but here's exactly what it does and doesn't do: - **It's read-only.** Its tools read the standard and change nothing, on our side or in your project. Your agent makes every change to your code itself, with the permissions you give it. - **It takes no credentials.** There's no account, API key or token, so there's nothing to set up, store or leak. - **It only sees what your agent sends it.** Each call carries a few arguments, like a component's name, a search query or the sections to read. The exception is the `code` and `screen` arguments of the review prompts, which send whatever you pass in. To keep your code on your machine, pass the paths of your files instead, like `src/components/Button.tsx`, and your agent reads them itself. - **It serves what the site publishes.** Everything it returns comes from the same files as this site, in its [public repository](https://github.com/uxfront-com/open-components){rel=""nofollow""}, so you can read any of it before your agent does. ## Troubleshooting ::accordion :::accordion-item{label="My client doesn't list the server, or its tools"} Check that the URL is exactly `https://mcp.opencomponents.dev/mcp`, with `/mcp` at the end, and that the client connects to it over HTTP, which some clients call streamable HTTP, rather than SSE. Then restart the client, or reload its window, since many only read their MCP settings when they start. Some clients keep remote servers under their own key. In `.vscode/mcp.json` and Visual Studio's `.mcp.json`, the server goes under `servers` rather than `mcpServers`, and Antigravity and Windsurf ask for a `serverUrl` rather than a `url`. Gemini CLI saves nothing when you add the server to a project from your home folder, so use `--scope user` there. ::: :::accordion-item{label="My client asks me to sign in"} The server takes no credentials, so there's nothing to sign in to. Choose none, which clients call **No sign in** , **No Authentication** or **None** , and if a command asks whether the server needs authentication, answer no. ::: :::accordion-item{label="My agent doesn't use the server"} Mention it by name, as in "using the open-components MCP server", at least the first time. In Claude and ChatGPT, check that it's turned on for the conversation, and in Visual Studio and JetBrains IDEs, that its tools are turned on for your agent (see [Editors](https://opencomponents.dev/#editors) ). ::: :::accordion-item{label="My client only runs local servers"} It can still reach this one through [mcp-remote](https://www.npmjs.com/package/mcp-remote){rel=""nofollow""}, which runs on your machine and passes every request on. Add it as a local server with this command: ```bash npx -y mcp-remote https://mcp.opencomponents.dev/mcp ``` ::: :::accordion-item{label="A page came back as an outline"} That's on purpose. A page longer than about 8,000 tokens comes back as its outline, so your agent can ask for the sections it needs rather than fill its context with the whole page. It usually does on its own, and naming your framework keeps only its examples. ::: :::accordion-item{label="The prompts don't show up"} Not every client supports MCP prompts. Codex doesn't, and Continue, OpenCode and Zed can't pass their arguments yet, so only `adopt-token-paths` works there. Ask for the same in your own words instead, as in [What you can ask](https://opencomponents.dev/#what-you-can-ask) : the tools are all your agent needs. Or install our [agent plugin](https://opencomponents.dev/docs/getting-started/agent-plugin) , whose skills your agent picks up on its own. ::: :::accordion-item{label="Opening the URL in a browser brings me here"} That's expected. The server answers MCP clients, and sends browsers to this page. ::: :::accordion-item{label="My agent doesn't support MCP"} It can still read the standard. Every page of these docs is published as markdown, every contract as YAML, and `/llms.txt` lists them all, as [For agents](https://opencomponents.dev/docs/getting-started/introduction#for-agents) in the introduction explains. ::: :::accordion-item{label="Can I run the server myself?"} Yes. It's in the `mcp/` folder of our [repository](https://github.com/uxfront-com/open-components){rel=""nofollow""} , and `pnpm dev:mcp` serves it on your machine, at `http://localhost:3100/mcp` by default. Our [contributing guide](https://github.com/uxfront-com/open-components/blob/main/.github/CONTRIBUTING.md#the-mcp-server){rel=""nofollow""} covers how it's built. ::: :: Still stuck? [Open an issue](https://github.com/uxfront-com/open-components/issues/new/choose){rel=""nofollow""} with your client, its version and what you tried, and we'll help you connect. # Agent Plugin Our agent plugin gives your agent the standard in one install: skills that build and review components the way the standard asks, and our [MCP server](https://opencomponents.dev/docs/getting-started/mcp-server), which they read it from. It follows [Agent Plugins](https://agent-plugins.org){rel=""nofollow""}, the open format for packaging skills and MCP servers, so the same plugin works in every client that supports it. [Install it](https://opencomponents.dev/#install-it) shows you how. The MCP server on its own gives your agent the standard as tools, but your agent still has to know when to reach for them, and the server's prompts only run when you start them. Skills are instructions your agent picks up on its own when a task calls for them. Ask it to build a dialog, or to review your checkout page, and it reads the contract, checks the work against every rule and cites each one by its ID, without being told how. The plugin is in [`plugins/open-components`](https://github.com/uxfront-com/open-components/tree/main/plugins/open-components){rel=""nofollow""} in our repository. It's free, with nothing to sign in to. ## Install it Most clients install it from our repository in one or two commands, from the marketplace file at its root, which lists the plugin. Then start a new session, so your agent picks up the skills. ### Claude Code ```bash claude plugin marketplace add uxfront-com/open-components claude plugin install open-components@open-components ``` That installs it for you, in every project. In a session, run the same as slash commands: ```text /plugin marketplace add uxfront-com/open-components /plugin install open-components@open-components ``` Its skills then start with `/open-components:`, as in `/open-components:build-component`, and `/mcp` lists the server as `plugin:open-components:open-components`. ### Codex ```bash codex plugin marketplace add uxfront-com/open-components codex plugin add open-components@open-components ``` Or run `/plugins` in Codex and install it from the marketplace's tab. Run `/mcp` to check that the server is connected. ### GitHub Copilot In Copilot CLI: ```bash copilot plugin marketplace add uxfront-com/open-components copilot plugin install open-components@open-components ``` In a session, use the same slash commands as in [Claude Code](https://opencomponents.dev/#claude-code). For Copilot's cloud agent on github.com, add the marketplace and the plugin to `.github/copilot/settings.json` in your repository. VS Code reads it too, and recommends the plugin to everyone who opens the repository: ```json { "extraKnownMarketplaces": { "open-components": { "source": { "source": "github", "repo": "uxfront-com/open-components" } } }, "enabledPlugins": { "open-components@open-components": true } } ``` ### VS Code Run **Chat: Install Plugin From Source** from the Command Palette and enter `https://github.com/uxfront-com/open-components`. VS Code finds the plugin in the repository and installs it, after asking you to trust the source. You can also add our repository to your marketplaces in `settings.json`, then search for `@agentPlugins` in the Extensions view and click **Install**: ```json { "chat.plugins.marketplaces": ["uxfront-com/open-components"] } ``` ### Cursor In the Cursor CLI, add our marketplace, then install the plugin from `/plugin`: ```bash agent plugin marketplace add https://github.com/uxfront-com/open-components.git ``` In the editor, copy the plugin into your local plugins folder, then run **Developer: Reload Window**: ```bash git clone --depth 1 https://github.com/uxfront-com/open-components.git /tmp/open-components cp -R /tmp/open-components/plugins/open-components ~/.cursor/plugins/local/open-components ``` ### Kiro Kiro calls plugins powers. Open the **Powers** panel, choose **Add Custom Power**, then **Import power from GitHub**, and enter the plugin's folder: ```text https://github.com/uxfront-com/open-components/tree/main/plugins/open-components ``` ### Hermes Agent ```bash hermes plugins install uxfront-com/open-components/plugins/open-components --enable ``` That downloads only the plugin's folder. Run `hermes plugins list` to check that it's enabled. ### OpenClaw ```bash openclaw plugins install open-components --marketplace uxfront-com/open-components ``` Run `openclaw plugins inspect open-components` to check it, which shows its format as Agent Plugins. ### OpenHands Under **Plugins**, click **Add plugin**, set **Source** to `github:uxfront-com/open-components` and **Path** to `plugins/open-components`, then click **Install**. With the SDK, pass the same as a plugin source: ```python PluginSource(source="github:uxfront-com/open-components", repo_path="plugins/open-components") ``` ### Other clients - **Any client that supports Agent Plugins** can load the plugin from a folder: clone our repository and point it at `plugins/open-components`. NanoClaw, for one, only takes templates from a folder, so copy it to `templates/open-components`, then run: ```bash ncl groups create --template open-components --name "Open Components" ``` - **Clients that run Agent Skills but not plugins** can still have the skills. This installs them in Claude Code, Codex, Cursor, Gemini CLI and dozens of other agents, from `/.well-known/skills/` on this site: ```bash npx skills add https://opencomponents.dev ``` :brIt leaves out the MCP server, so [connect it](https://opencomponents.dev/docs/getting-started/mcp-server#connect-it) yourself, or let the skills read the standard from this site. ## What it offers ### Skills Your agent picks a skill when your request matches what it's for. In most clients, you can also start one yourself, as a slash command like `/build-component`. | Skill | What it's for | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `build-component` | Builds a component in your framework, from its contract, its reference implementation and its tests, or, for one on the [Roadmap](https://opencomponents.dev/docs/getting-started/roadmap), following the Button's structure | | `review-component` | Reviews a component against its contract, or your styles against [Design Tokens](https://opencomponents.dev/docs/foundations/design-tokens), reporting every rule as pass or fail with its ID and the evidence, then fixes the failures | | `review-usage` | Does the same for a screen that uses a component, against the rules the screen meets, like keeping one primary button per view | | `adopt-token-paths` | Renames your CSS variables to [token paths](https://opencomponents.dev/docs/foundations/design-tokens), then adds the Stylelint rule that keeps them that way | They ask for what the MCP server's [prompts](https://opencomponents.dev/docs/getting-started/mcp-server#prompts) ask for, under the same names, then follow the steps the prompts point your agent at. ### The MCP server The plugin connects our MCP server, at `https://mcp.opencomponents.dev/mcp`, under the name `open-components`. The [MCP Server](https://opencomponents.dev/docs/getting-started/mcp-server#what-it-offers) page covers its tools, prompts and resources. If you've already added the server yourself, remove that entry once the plugin is installed, so your agent doesn't see every tool twice. Where a client doesn't connect the MCP servers in a plugin, or can't reach ours, the skills read the same contracts and pages from this site, at their [`/raw`](https://opencomponents.dev/docs/getting-started/introduction#for-agents) URLs. ## An example Once the plugin is installed, just ask: ```text Review our Button against Open Components. ``` Your agent picks up `review-component`, reads the Button's contract with `get-contract`, and lists the rules the component itself meets with `list-rules`. Then it reports every rule as pass or fail, with its ID and the evidence from your code, and fixes the failures. # Design Tokens Design Tokens are the named decisions behind every component, such as its colours, sizes, spacing and corner radii. In CSS, they're custom properties, and since their names are all that people, tools and agents have to go on, the names matter as much as the values. The biggest problem is that a name like `--blue-2` doesn't say what it's for, and `--color-primary-text` can be read in more than one way. Is it the `primary-text` colour, or the `text` of `color-primary`? This page covers how every Open Components token is named, a convention we call **token paths**. We'll look at: - **Token paths** - how to name a token, and why with a double dash - **Semantic names** - how to name a token after what it's for - **Theme tokens** - the colours that every component shares - **Component variables** - how a component names its own variables - **Checking tokens** - how linters and agents can hold your tokens to all of this ## At a glance | Aspect | Tokens | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | Convention | Token paths, as in `--{group}--{name}` | | Separators | `--` between groups, and `-` between the words inside a group or a name | | Characters | Lowercase letters and digits | | Names | Semantic, saying what a token is for rather than what it looks like | | Theme | `--color--{color}`, `--color--{color}-contrast`, `--color--{color}-text` and `--color--focus` | | Components | `--{component}--{name}`, or `--{component}--{part}--{name}` for a part, set on the component's root | | Tools | `{color.primary}` in DTCG, `color/primary` in Figma and `color.primary` in [Styleframe](https://styleframe.dev){rel=""nofollow""} | | Lint | Stylelint's `custom-property-pattern` | ## Token paths A token's name is its path, made of the groups it belongs to, followed by its own name. Groups are separated by a double dash, and the words inside a group or a name by a single dash. ```css :root { --color--primary: …; /* color › primary */ --color--primary-contrast: …; /* color › primary-contrast */ --font-size--sm: …; /* font-size › sm */ } .button { --button--icon--size: 1rem; /* button › icon › size */ } ``` We call this convention token paths because the double dash plays the part of the dot in a path. The path stays the same wherever your tokens live, so moving between your design files, your token files and your CSS only means swapping the separator: | Where | How it's written | | -------------------- | ----------------------- | | CSS | `--color--primary-text` | | Design tokens (DTCG) | `{color.primary-text}` | | Figma variables | `color/primary-text` | | Styleframe | `color.primary-text` | ### Groups Most tokens only need two levels, the first saying what they belong to, and the second what they are. - The first group is the token's scope. It's either a category, like `color`, `spacing`, `font-size` or `border-radius`, or a component, like `button`. - The last part is the token's name, like `primary`, `md` or `height`. - A variation of a token stays in its name. The label on the primary fill is `--color--primary-contrast`, and a darker shade would be `--color--primary-shade-50`, rather than `--color--primary--contrast` or `--color--primary--shade-50`. When a token belongs to a part or a state of a component, rather than to the component as a whole, give that part or state a group of its own, even for a single token. The button sizes its icons with `--button--icon--size`, a tooltip's arrow would have `--tooltip--arrow--size`, and a link's hover state `--link--hover--color`. Since the group is there from the first token, adding a second one later, like `--button--icon--color`, never means renaming the first. A CSS property stays in one piece, though. `font-size` is a single name, so the button's text is sized with `--button--font-size`, rather than `--button--font--size`, and the same goes for names like `border-radius` and `line-height`. ### Why a double dash The double dash is what makes a name readable in both directions, by people and by tools. - **It's unambiguous.** With single dashes, `--font-size-sm` could be `font.size-sm`, `font-size.sm` or `font.size.sm`, and a tool has no way to tell which. `--font-size--sm` can only be `font-size.sm`, so tools, and agents, can turn any name back into its path. It's the same reason the [DTCG format](https://www.designtokens.org/tr/2025.10/format/){rel=""nofollow""} doesn't allow dots in token names. - **It's searchable.** Searching for `--button--` finds every variable of the button and nothing else, while `--button-` would also find a button group's `--button-group--gap`. - **It doesn't clash.** Tailwind CSS names its theme variables with single dashes, like `--color-blue-500` and `--radius-lg`, and libraries built on it add their own, like daisyUI's `--color-primary`. A token path can never have the same name as one of them, so your tokens and theirs can share a page. ## Semantic names Name a token after what it's for, rather than what it looks like. `--color--primary` still makes sense after a rebrand and in dark mode, while `--color--indigo-600` doesn't. It's the same idea as the Button's [`color`](https://opencomponents.dev/docs/components/button#colours) [prop](https://opencomponents.dev/docs/components/button#colours), which describes an intent rather than a hue. Tokens come in up to three tiers, and each tier reads from the one before it: | Tier | What it holds | For example | Read by | | --------- | ----------------------------- | ------------------------------------- | -------------- | | Palette | Every value you have | `--color--indigo-600`, `--spacing--4` | The theme | | Theme | What each value is for | `--color--primary`, `--color--focus` | Components | | Component | One component's own decisions | `--button--height`, `--button--fill` | That component | The palette is optional. Our reference tokens skip it, and set the theme straight from values in the Tailwind CSS palette. What matters is that components only read theme tokens, so a new brand or a dark mode is a new set of theme values, and no component has to change. ```css /* Do: the button reads what the colour is for */ .button[data-color="primary"] { --button--fill: var(--color--primary); } /* Don't: it reads a colour from the palette, so a new brand means editing every component */ .button[data-color="primary"] { --button--fill: var(--color--indigo-600); } ``` ## Theme tokens Every component reads its colours from the same theme tokens, so a single theme styles them all. There are three tokens per colour, plus a focus colour: | Token | What it is | | --------------------------- | ----------------------------------------------------- | | `--color--{color}` | The colour itself, for fills and tints | | `--color--{color}-contrast` | Text and icons on top of the fill | | `--color--{color}-text` | Text and icons in that colour, straight on the page | | `--color--focus` | The focus ring, which is the same for every component | `{color}` is one of `primary`, `secondary`, `neutral`, `success`, `info`, `warning` and `error`, the same colours as the `color` prop. A component with `color="primary"` reads `--color--primary`, `--color--primary-contrast` and `--color--primary-text`. Each pairing has to stay readable. `-contrast` needs a contrast of 4.5:1 against its colour, `-text` needs 4.5:1 against the page ([WCAG 1.4.3](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html){rel=""nofollow""}), and `--color--focus` needs 3:1 against the page ([WCAG 1.4.11](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html){rel=""nofollow""}), in both light and dark mode. Components that mix these colours, like the Button's `soft` variant, check their own pairings on top of that. The tokens use `light-dark()`, which follows `color-scheme`, so a single token holds the value for both modes: ```css :root { --color--primary: light-dark(oklch(51.1% 0.262 276.966), oklch(67.3% 0.182 276.935)); --color--primary-contrast: light-dark(oklch(100% 0 0), oklch(14.1% 0.005 285.823)); --color--primary-text: light-dark(oklch(45.7% 0.24 277.023), oklch(78.5% 0.115 274.713)); /* …secondary, neutral, success, info, warning and error */ --color--focus: light-dark(oklch(51.1% 0.262 276.966), oklch(67.3% 0.182 276.935)); } ``` You'll find the full set, with values that pass in both modes, in the Button's [`tokens.css`](https://opencomponents.dev/docs/components/button#reference-implementation). ## Component variables A component names its own variables after itself, as `--{component}--{name}`, or `--{component}--{part}--{name}` for one of its parts, and sets them on its root: ```css .button { --button--height: 2.25rem; --button--radius: 0.5rem; --button--icon--size: 1rem; --button--fill: var(--color--neutral); } .button[data-size="sm"] { --button--height: 2rem; --button--radius: 0.375rem; } .button[data-color="primary"] { --button--fill: var(--color--primary); } ``` - Setting them on the root, rather than on `:root`, keeps them with the component. Each size or colour changes them with a single selector, and nothing leaks into the rest of the page. - Starting them with the component's name makes it clear who owns them, so `--button--fill` can never be mistaken for a theme token. - Their colours come from the theme tokens, so the component follows every theme on its own. To tune a component, set its variables from a class of yours rather than with an inline style, since inline styles can't be reused, can't respond to media queries and are blocked by a strict Content Security Policy. Components put their styles in the `components` [cascade layer](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer){rel=""nofollow""}, and a style outside a layer beats a layered one whatever its specificity, so a single class is all it takes: ::framework-switcher #react ```tsx [GetStarted.tsx] import styles from "./GetStarted.module.css"; export function GetStarted() { return ( ); } ``` ```css [GetStarted.module.css] .pill { --button--radius: 999px; } ``` #vue ```vue ``` #svelte ```svelte ``` #angular ```angular-ts @Component({ selector: "app-get-started", imports: [Button], template: ``, styles: ` .pill { --button--radius: 999px; } `, }) export class GetStarted {} ``` #solid ```tsx [GetStarted.tsx] import styles from "./GetStarted.module.css"; export function GetStarted() { return ( ); } ``` ```css [GetStarted.module.css] .pill { --button--radius: 999px; } ``` #astro ```astro ``` #vanilla ```html ``` :: The layers follow the same order as Tailwind CSS (`theme`, `base`, `components` and `utilities`), so components win over its resets and give way to its utilities. Keep in mind that a reset of your own outside a layer, like `button { border-radius: 0 }`, would win too, so it's best to put your resets in the `base` layer. Every component page covers its own variables under Tokens, as the [Button](https://opencomponents.dev/docs/components/button#tokens) does. ## Checking tokens The naming can be checked automatically, and agents can check the rest against the contract below. ### Lint Stylelint's [`custom-property-pattern`](https://stylelint.io/user-guide/rules/custom-property-pattern/){rel=""nofollow""} rule checks every custom property against a pattern, without its leading `--`. This one accepts token paths and nothing else: ```json [.stylelintrc.json] { "rules": { "custom-property-pattern": [ "^[a-z0-9]+(-[a-z0-9]+)*(--[a-z0-9]+(-[a-z0-9]+)*)+$", { "message": "Name CSS variables as token paths, like --color--primary" } ] } } ``` It flags `--color-primary`, `--button-height`, `--Color--primary`, `--color---primary` and `--primary`, and it checks `var()` references as well as declarations. That means it flags other libraries' variables too, like Tailwind's, so run it on your theme and your components' styles. ### Described Here's the whole convention in one block, for agents to read before they write or review a token: ```yaml convention: token-paths standard: https://opencomponents.dev/docs/foundations/design-tokens format: "--{group}--{name}" # "--{group}--{part or state}--{name}" for a part or state, even with one token separators: { group: "--", word: "-" } pattern: "^--[a-z0-9]+(-[a-z0-9]+)*(--[a-z0-9]+(-[a-z0-9]+)*)+$" paths: css: "--color--primary" dtcg: "{color.primary}" figma: color/primary styleframe: color.primary theme: colors: [primary, secondary, neutral, success, info, warning, error] tokens: ["--color--{color}", "--color--{color}-contrast", "--color--{color}-text", "--color--focus"] components: "--{component}--{name}, or --{component}--{part}--{name}, set on the component's root" overrides: "A class of yours, outside a layer. Components style themselves in @layer components, in Tailwind's layer order." rules: https://opencomponents.dev/docs/foundations/design-tokens#checklist ``` It's also published on its own at [`/raw/docs/foundations/design-tokens.yaml`](https://opencomponents.dev/raw/docs/foundations/design-tokens.yaml){external=""}, with every rule from the checklist in place of the `rules` link. ### Prompts You can point your agent straight at this page. To move your variables over to token paths, you could use: ```text Rename our CSS variables to follow the Open Components token paths: https://opencomponents.dev/raw/docs/foundations/design-tokens.md List our token groups first, then rename every variable along with every var() that reads it, keeping the values as they are. Then add the Stylelint rule from the page, and fix anything it reports. ``` And to review them: ```text Review our theme and component styles against the Open Components tokens standard: https://opencomponents.dev/raw/docs/foundations/design-tokens.md For each rule in its checklist, report pass or fail with the rule ID and the evidence from our code. Then fix the failures. ``` ## Checklist Here's every rule in one place, along with where it comes from and how to check it. Your tokens meet the standard when they meet every **must**, and each **should** is expected unless you have a good reason not to follow it. The basis says where a rule comes from. Contrast rests on **WCAG** 2.2, at level AA, while the naming rules are our own, marked **Open Components**, since WCAG has nothing to say about how you name your variables. | Rule | Level | Requirement | Basis | Check | | ------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ---------------- | | `tokens/token-paths` | Must | Variables are named as token paths, in lowercase, with `-` between words and `--` between groups | Open Components | Stylelint | | `tokens/semantic-names` | Must | Theme and component tokens say what they're for, rather than what they look like | Open Components | Review | | `tokens/theme-only` | Must | Components read theme tokens, and never palette tokens or raw colours | Open Components | Review | | `tokens/shared-theme` | Must | Components read the shared theme tokens, so a single theme styles them all | Open Components | Review | | `tokens/component-scope` | Must | A component's own variables start with its name, like `--button--`, and are set on its root | Open Components | Review | | `tokens/class-overrides` | Should | Components put their styles in the `components` cascade layer, so a class can set their variables without an inline style | Open Components | Review | | `tokens/contrast` | Must | `-contrast` has 4.5:1 against its colour, `-text` 4.5:1 against the page and `--color--focus` 3:1, in light and dark mode | WCAG 1.4.3, 1.4.11 (AA) | Contrast checker | | `tokens/groups` | Should | A part or state always gets its own group, like `--button--icon--size`, while a variation or a CSS property stays in one name | Open Components | Review | ## FAQ ::accordion :::accordion-item{label="Isn't the double dash a BEM modifier?"} Not here. In BEM, `--` marks a modifier, as in `.button--primary` . Token paths use it the way a path uses a dot, so `--color--primary` reads as the `primary` token in the `color` group, rather than a colour modified to be primary. BEM names classes and token paths name variables, so the two can live side by side. ::: :::accordion-item{label="Can I use token paths with Tailwind CSS?"} Yes. Tailwind reads any variable you put in parentheses, as in `bg-(--color--primary)`. If you'd like shorter utilities, you can map your tokens onto its theme: ```css @theme inline { --color-primary: var(--color--primary); } ``` With that, `bg-primary` sets `background-color: var(--color--primary)`, so your tokens stay the single source of truth. ::: :::accordion-item{label="Why three tokens per colour?"} Because one colour can't be readable on both sides. `--color--warning` is a bright yellow, so the text on top of it has to be dark, and on a white page, warning text needs a much darker shade than the fill. With `-contrast` and `-text` , each pairing gets a colour that passes on its own, in both light and dark mode. ::: :::accordion-item{label="How do I move from single-dash names?"} One group at a time. A script can't tell where a single-dash name's group ends, which is exactly the problem token paths solve, so start by listing your groups, like `color` and `button`. Then replace `--color-` with `--color--`, and so on, in declarations and `var()` references alike. With Perl, for example: ```sh perl -pi -e 's/--(color|button)-(?=[a-z])/--$1--/g' $(git grep -lE -e '--(color|button)-[a-z]') ``` The Stylelint rule above will then catch anything you've missed. ::: :: ## Sources - [Design Tokens Format Module](https://www.designtokens.org/tr/2025.10/format/){rel=""nofollow""}, for token paths and groups - [CSS Custom Properties for Cascading Variables](https://www.w3.org/TR/css-variables-1/){rel=""nofollow""} - [Styleframe: Variables](https://www.styleframe.dev/docs/api/variables){rel=""nofollow""}, for the dot notation and its double-dash output - [Figma: Overview of variables, collections and modes](https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes){rel=""nofollow""} - [Material Design 3: Design tokens](https://m3.material.io/foundations/design-tokens/overview){rel=""nofollow""}, for the tiers - [Tailwind CSS: Theme variables](https://tailwindcss.com/docs/theme){rel=""nofollow""} - [Stylelint: custom-property-pattern](https://stylelint.io/user-guide/rules/custom-property-pattern/){rel=""nofollow""} - [Web Content Accessibility Guidelines (WCAG) 2.2](https://www.w3.org/TR/WCAG22/){rel=""nofollow""} # Radio Group Radio groups let people pick one option out of a few, like a delivery speed or a plan, while every option stays in view. They look like a list of circles with words beside them, which is why they're so often built from scratch. Think of radios that each have their own `name`, so people can check all of them at once, a question that's a heading rather than a legend, so screen readers never read it, or a group that turns red when people skip it, but doesn't tell screen reader users anything at all. This page walks you through building one the right way. We'll look at: - **User Interface** (UI) - how a radio group looks - **User Experience** (UX) - how it behaves for every person and every input - **Developer Experience** (DX) - how developers work with it - **Agentic Experience** (AX) - how agents can read and verify it Every example on this page runs on our [reference implementation](https://opencomponents.dev/docs/components/radio-group#reference-implementation), which does its part of every rule in the [checklist](https://opencomponents.dev/docs/components/radio-group#checklist), so feel free to click and arrow through them as you read. The code under each example comes in React, Vue, Svelte, Angular, Solid, Astro and Vanilla, written for a Radio Group with the same API in each. Pick yours from the Framework select above the sidebar. Vanilla is plain HTML and JavaScript, so it writes out the markup from the [DOM contract](https://opencomponents.dev/docs/components/radio-group#deterministic). The Radio Group is the question, and each Radio inside it is one answer. The group renders its legend, its hint and its error message, since a fieldset needs its legend inside it, and its radios share its name, its size and its value, so they can never disagree. :radio-group-playground ## At a glance | Aspect | Radio Group | | -------- | --------------------------------------------------------------------------------------------------------------------------------- | | Element | `
` with a ``, around native ``s, each inside its `