For agents
How a coding agent finds, uses and checks brand-ui: a live tool-call trace, the generative-UI editor, and the install matrix for every host.
For agents
Ask your agent — and watch the calls
Pick a prompt and run it. The page calls the same hosted MCP server your coding agent would, shows each call as a tool card, then renders the block the answer points to — in the theme you have on.
Tool calls
Run a prompt to see the MCP calls it makes.
The rendered block appears here.
brand-ui never owns model calls. This demo calls the same hosted MCP your agent would; the picking was done ahead of time. Why: decision D5
Let the agent emit the UI
Edit what an agent would emit, and watch it validate and render
The editor runs the library’s own validator on every pause in your typing. The errors below it are the validator’s, word for word.
A surface the agent designs at runtime, validated against the catalog, rendered by the library.
brand-ui a2ui schemaWorks with your agent
Six ways in, one spec — pick your host once and every command below matches it.
claude mcp add --transport http brand-ui https://elabs-ai.com/mcpnpx -y @elabs-ai/components-cli mcp/plugin marketplace add mreimitz/elabs-componentsThe daily routine
Install it your way
Gates, not guidelines (98)
Stories8 rules
a11y-baseline— Axe stays blocking: preview.tsx keepsa11y: { test: "error" }and appliesscripts/a11y-baseline.json, whose generated per-story exemptions never exceedratchet.maxStories; fix a new violation, never exempt it.chart-unit-caption— Every unit-decomposed chart (waffle/field UnitChart, dot heatmap,unit-ed Bar/WaterfallChart, beaded DumbbellChart, rawUnitStack) in a chart story, aregistry/blocks/**block or a block story states its unit ("one X = N") inunitLabel,descriptionoraccessibleDescription.loading-states— A component with aloading/isStreaming(or chartstatus: ChartStatus) prop ships a story that shows it: a*Loading/*Streamingexport or a not-ready arg (loading: true,status="loading").no-fixed-story-wrapper— Story decorators and render wrappers never pin a fixed width above 320px (w-[800px],style={{ width: 800 }}) without a max — usew-full max-w-*orparameters.layout.state-coverage— Every allowlisted stateful component (STATEFUL_COMPONENTSin the rule) exports a story named for a non-happy state (Loading,Empty,Error,Disabled,Skeleton,FirstRun,Awaiting).story-name-hygiene— A story's visible name says what the story shows — never an issue number (#123), anADR, anRM-roadmap item,harnessorregression; that reference belongs in a comment orparameters.docs.description.story.story-viewport-guard— A story meta for a layout-level surface (Layout/*,Patterns/Templates/*,Patterns/Scenarios/*, AppShell, Sidebar, DataTable, ChatShell, Hero, …) setsparameters.layout("fullscreen"/"padded") or a viewport — never the centered default.variant-coverage— Render everycvavariant value in a story (variant="success"orargs: { variant: "success" });argTypes.optionsdoes not count, a default is met by a story that leaves the axis unset.
Packages24 rules
agent-output-contract— The manifest'sagentOutputcontract matchesstatusFromToolState(aitool.tsx) andSTATUSES(status-badge.tsx), names only real@elabs-ai/components-aiexports, and its example never callsuseChat(.ai-sdk-types-only—@elabs-ai/components-aiimportsai/@ai-sdk/*as types only (import type, inlinetypespecifiers); runtime values likeuseChatbelong in the consuming app (ADR 0008, D6).attribution-provenance— When shipped source says code was adapted/vendored/borrowed/forked/copied/ported from somewhere, credit that upstream inscripts/attributions.sources.json(thenpnpm gen) in the same change.charts-definition-isolation— No chart/part/surface family imports another family's definition, and only AutoChart,definitions/components.tsandsrc/test/**import the definition registry — a family stays tree-shakeable on its own (ADR 0042 §11; runtime imports read with the TypeScript parser,import typeexempt; test/story/fixture files exempt).charts-definitions-pure— The runtime import closure of chartsdefinitions/**andcharts/props/**reaches only@elabs-ai/components-ui/definitionand allow-listed pure leaves — never React/visx/d3/motion/ui at runtime (import typeexempt; test/fixture files anddefinitions/components.tsexempt as roots only; ADR 0042 §11).charts-reuse— In@elabs-ai/components-charts, never import@base-ui/*(a runtime export named like a@elabs-ai/components-uicomponent isui-reuse's finding).charts-test-double— Keep@elabs-ai/components-charts/testa faithful, engine-free double: export every real component fromsrc/test/index.ts, never import @visx/d3/motion or a chart barrel at runtime fromsrc/test/**, keep./testin exports + publishConfig.exports + tsup, and keep/testsubpaths out of the manifest.dep-direction—@elabs-ai/components-*runtime deps (dependencies/peerDependencies) follow the one-way DAGtokens → ui/icons → layer-2 leaves → process; every package is registered inALLOWED, and a shared piece moves down, never sideways.eager-heavy-deps— Reach heavy engines (mermaid, Rive, xterm, React Flow, viewer parsers) and a package's own optional peers only via dynamicimport()or a@lazy-boundarymodule inai/terminal/viewersrc; never import a@lazy-boundarymodule statically.home-bundle— Keep the website's/initial JavaScript (gzip,apps/home/.next) within thehome-bundlebudget inscripts/check/baseline.jsonand free of per-section engines (@xyflow, monaco-editor, maplibre-gl, @milkdown, components-process, components-terminal); skipped without a build unlessHOME_BUNDLE_REQUIRED=1.home-imports— Build the website (apps/home, ADR 0038) only from the library: import react, react-dom, next, motion, @vercel/analytics, lucide-react,@elabs-ai/*or a relative path; a registry block copied underapps/home/components/blocks/keeps its// registry: <name> — copied <YYYY-MM-DD>header.lucide-version— Declare ONElucide-reactversion specifier across every workspace manifest (deps, peers, dev deps,pnpm.overrides,resolutions).media-reuse— No raw<img>,<video>or<audio>in a package outsideui'simage/andmedia-player/folders — composeImage,Video,Audiofrom@elabs-ai/components-ui; exempt one line with// media-reuse-exempt: <reason>.optional-peer-transitives— An optional peer is not also installed through a plain transitive dependency (resolved frompnpm-lock.yaml); the known, disclosed exceptions in the rule'sKNOWN_DEFEATSare exact — remove one the day it goes clean.process-reuse— In@elabs-ai/components-process(layer 3), compose base packages: no local component named like a ui/flow/charts/data export, no raw SVG primitives, no@xyflow/reactprimitive flow wraps, no ai/maps/marketing/editor/viewer/terminal imports, and no engine or React import undersrc/core/; exempt one line with// process-reuse-exempt: <reason>.process-test-double— Keep@elabs-ai/components-process/testcomplete and engine-free: re-export every double fromsrc/test/index.ts, never import React Flow/visx/d3/motion or flow/charts/data/process barrels at runtime fromsrc/test/**, keep./testin exports + publishConfig.exports + tsup, and keep/testsubpaths out of the manifest.remote-origins— Declare everyhttps://origin shipped source can reach (including upstream URLs inattributions.generated.ts) inscripts/remote-origins-allowlist.jsonwith its kind, CSP directive and escape hatch, and name it indocs/CSP-AND-NETWORK.md.sanitizer-passthrough— Wrap a safe-by-default renderer (Streamdown) only withOmit<…, "rehypePlugins">on every props type andstripSanitizerOverrides(props)before spreading props onto it; never setrehypePluginsoutside the reviewed allowlist.tailwind-sources— Name every package that ships Tailwind classes in each@source-bearing app CSS file with a pattern that resolves topackages/<name>/src(ornode_modules/<pkg>/dist); an unlisted package renders unstyled.trusted-types-patches— Keep@radix-ui/react-scroll-areaand@radix-ui/react-selectpatched (registered inpnpm.patchedDependencies, installed dist free of HTML sinks); after a version bump re-apply the patch withpnpm patch.trusted-types-sinks— Never assign HTML (dangerouslySetInnerHTML,.innerHTML =,insertAdjacentHTML,document.write) in package source or add a dependency that does — it blanks a Trusted-Types app; static markup belongs in CSS or JSX, and an unavoidable engine sink is baselined and documented indocs/CSP-AND-NETWORK.md.ui-reuse— A layer-2 package (ai, data, flow, maps, charts, marketing, editor, viewer, terminal) never declares a runtime export named like a@elabs-ai/components-uicomponent — import it or use a package-scoped name; a pass-through alias (export { X as Y } from …) is fine; exempt one line with// ui-reuse-exempt: <reason>.use-client-source— A client package (ui,data,ai,flow,maps,charts,editor,viewer,terminal) whose source uses React hooks carries"use client"in its source modules, not only in the build banner.viewer-highlight— A viewer adapter'scapabilities.highlightlists only real address kinds, its renderer readshighlights(andrectsforrect), and its*-adapter.test.tsxbuilds and paints each declared kind.
Repo14 rules
agent-token-budget— Keep what an agent reads first under its byte ceiling: the brand-ui skill router (8 KiB),llms.txtand eachllms/<pkg>.txt, a created app'sbrand-ui-context.mdfor every template, and every component'sdocs --briefcard.catalog-visitor-copy— Keep the website catalog's page leads, about paragraphs and subtitles free of maintainer prose (roadmap items, ADRs, issue numbers, fixtures, story ids, repo paths) and give every page a lead — author apurposeinpackages/cli/lib/intent.mjsor adocs.description.componentin the story.charts-deprecated-usage— No deprecated chart prop name (an alias row of the definition snapshot, e.g.<HeatmapChart showValues>once it is renamed) in stories, Markdown code fences, templates, registry blocks or non-test package source — write the new name, so consumers never see a deprecation warning for code they did not write; tests, the./testdouble and history docs are exempt.conflict-markers— Resolve every Git merge conflict before committing: no line may start with a<<<<<<</=======/>>>>>>>marker.csp-policy— Keepdocs/csp-policy.jsonand thecsp:published/csp:devblocks indocs/CSP-AND-NETWORK.md§2.7 identical, and justify every non-'self'relaxation with a carve-out whosewhynames the reason.lockfile-dup-keys—pnpm-lock.yamlnever repeats a sibling mapping key (a bad merge breaks--frozen-lockfileand silently stops CI); dedupe or regenerate withpnpm install --lockfile-only.machine-paths— Never commit a machine-specific absolute home path (/Users/<name>/…,/home/<name>/…); write it relative to the repo root.no-biome-ignore— Suppress lint findings with// eslint-disable-next-line <rule> -- <reason>, never abiome-ignorecomment (this repo has no Biome, so it is inert).plugin-consumer-clean— Shipped plugin skills and agents reference no repo-internal plumbing (/file-issue,.claude/,packages/,apps/, maintainer agents); end users install them without this monorepo.plugin-manifest— The Claude plugin installs whole:plugin.jsonandmarketplace.jsonagree on version, every declared skill/agent path starts./and resolves, thebrand-ui-startrouter is user-invocable, MCP servers are http or stdio, and shared skill docs exist exactly once.pnpm-script-refs— Apnpm <script>named in code, config or docs must be a real script (root or workspace package); dated records are exempt.reference-leakage— Name a third-party product (Datawrapper, Qlik, Grafana, MUI, …) only in an attribution or theme surface; everywhere else — shipped source, the manifest, changesets, docs, roadmap — say what the feature does instead. Package specifiers and the librariesbrand-ui migrateconverts from are exempt.test-concurrency— The roottestscript runsturbo run test --concurrency=<int>; never raise a vitesttestTimeoutto absorb CPU oversubscription (#80).tt-aliases— Every app aliasesdecode-named-character-referenceandhast-util-from-html-isomorphicviarequire.resolve(…), declares both as direct devDependencies, anddocs/CSP-AND-NETWORK.mdstill documents both.
Components37 rules
ai-microcopy-a11y— Every literalaria-labelandplaceholderin@elabs-ai/components-aigoes throught()— no baseline, no exceptions beyond// i18n-exempt: <reason>.chart-default-prose— A default stated in a chart prop's TSDoc (Default: 1100,Default"none",@default false) matches that prop's definition default in the snapshot — when they disagree, fix the prose, never the default.chart-hairline— Draw chart furniture with--chart-gridat full opacity andCHART_HAIRLINE_WIDTH(never a dimmingopacity/strokeOpacityor another numericstrokeWidth), and never alias--chart-gridtovar(--border); one element may opt out with// chart-hairline-exempt: <reason>.chart-style-constants— Name chart dash, opacity and duration values through the shared constants (CHART_DASHinchart-stroke.ts,chart-opacity.ts,animation.ts) instead of literals; the count of literals left inpackages/charts/srconly goes down.charts-group-drift— A chart definition takes a prop named like a prop-group key (palette,status,align,plotHeight, …) from that group — apply the group; an own field of the same name then overrides it — never as a family field of its own; the keys that drift today only shrink, and the one exception list (zoomon Choropleth, Tree and DensityScatter, ADR 0042 A.5) grows only by a maintainer decision.charts-honesty— In@elabs-ai/components-charts, draw bars from a zero-including domain (resolveBarValueDomain/resolveYDomain(…, { includeZero: true })), scale area radii bysqrt(value / max), and never callMath.random(useseededRnd); a reasoned exception carries// honesty:allow <reason>.charts-responsive— Every@elabs-ai/components-chartscontainer publishes its breakpoint tier (rendersChartPlotRoot, setsdata-chart-breakpoint, or wraps a container that does), and aResponsive<…>prop is read only throughresolveResponsive/useResponsiveValueor handed on whole — never branched on directly (ADR 0039); opt out with// charts-responsive-exempt: <reason>.collapse-fork— Build a collapsing side panel onuseCollapsiblePanel(@elabs-ai/components-ui); never hand-roll atransition-[…width…]tween plus an off-screen[calc(var(--x)*-1)]slide outsidepackages/ui/src/components/collapsible-panel.component-registration— Every@elabs-ai/components-uicomponent folder is re-exported fromsrc/index.ts(or its own subpath export) and ships a*.stories.tsx.contract-known-failures— Every scripts/check/contract-known-failures.json entry is a well-formed, ratcheted contract-test gap; the shared baseline makes the count visible so growing it is a reviewed move, not a silent mute.data-slot— Give every exported component's rootdata-slot="<kebab-name>"and each sub-partdata-slot="<kebab-name>-<part>"; a module never gains a component without a slot or drops a slot without its part.disabled-recipe— A dimmed disabled state follows the house recipe:disabled:opacity-50plusdisabled:pointer-events-none(Button) ordisabled:cursor-not-allowed(Input) in the same class list.effect-cleanup— AuseEffect/useLayoutEffectthat adds a listener, starts a timer/interval, creates an observer or subscribes returns a cleanup that removes/clears/disconnects/unsubscribes it.focus-ring-only— Focus indicators usefocus-ring/focus-ring-within/focus-ring-inset/focus-ring-static, never a hand-rolledfocus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ringstack.forward-ref-required— An exported component that spreads...propsonto a DOM element is wrapped inforwardRef.header-band— Size every header band (<header>, or adata-slotending-header/top-bar, with aborder-b) withh-header, neverh-14/h-12or padding, so side-by-side shell headers share one bottom line in every theme; opt out with// header-band-exempt: <reason>.home-tokens— Paint the website (apps/home, ADR 0038) with tokens only: no raw hex,rgb()/hsl()/oklch()colour or arbitraryduration-[…]/ease-[…]inapps/home/**/*.{ts,tsx,css};app/globals.cssmay only referencevar(--…).i18n-strings— UI text in package source (JSX text,aria-label/title/placeholderliterals) comes from props or a labels object so apps can localize it; stories, tests, templates and registry are exempt.locale-formatting— Format numbers and dates withIntl.*and a locale prop: notoLocaleString()/toLocaleDateString()/toLocaleTimeString()without a locale, nonew Date(…).toString()in JSX.logical-props— Use logical direction utilities (ms-/me-/ps-/pe-/start-/end-/border-s/rounded-s/text-start), never physicalml-/pr-/left-/border-l/text-right, so layouts mirror in RTL.memo-on-children— Never wrap a component whose props includechildreninmemo/React.memo— inline children defeat the shallow compare; memoise the children-free part instead.microcopy— Route user-visible strings (aria-label,placeholder,title, JSX text) through the locale seamt()(ADR 0017); a genuinely untranslatable string carries// i18n-exempt: <reason>.microtypography-apostrophe— Write the curly apostrophe ’ (never a straight'between letters) in JSX text andaria-label/placeholder/title/descriptionvalues, stories included; opt out with// microtypography-exempt: <reason>.microtypography-ellipsis— Write "…" (U+2026), never "...", in JSX text andaria-label/placeholder/title/descriptionvalues, stories included; a genuine code sample carries// microtypography-exempt: <reason>.motion-tokens— Use motion tokens in charts/ai source and the website (apps/home):duration-fast|base|slow|slowerandease-standard|entrance|exit, neverduration-<N>,ease-in/ease-out/ease-in-outortransition-all(docs/MOTION_GUIDELINES.md).no-fixed-trigger-width— Triggers (*Triggercomponents,data-slot="*-trigger") size withmin-w-*/w-full, never a fixedw-40/w-[180px].no-index-key-reorderable— List keys are stable ids from the item, never the.mapindex (key={i}); placeholder lists (Array.from({ length }),(_, i)) are exempt.radius-rungs— Radius comes from therounded-*scale (backed by--radius), never an arbitraryrounded-[6px].raw-palette— Use semantic color utilities (text-info-text,bg-success/10,border-destructive), never raw Tailwind palette utilities (text-yellow-600,bg-red-500) in package source and the website (apps/home).source-theme-count— Say "every theme" (or "both themes"), never a hardcoded theme count that disagrees withBUILT_IN_THEMES, in package source comments and stories.status-rung— Paint running status text with the ink rungtext-<tone>-text; a baretext-<tone>(the 3:1 fill rung) is for marks only and never shares a class string with a text tell (text-xs, a type role,font-*,truncate).surface-separation— Give each region ONE separation gesture: never a bareborderin the same class string as a non-default fill (bg-surface-muted,bg-surface-elevated,bg-chat-user,bg-<status>/N); rails (border-s-*), axis dividers andborder bg-cardare fine.text-scale— Set type with a role (text-display-lg|display|title|subtitle|heading-xs|body|caption|meta|eyebrow|kpi|kpi-sm|codeor<Heading>/<Text>), never a raw size utility (text-sm,text-xl,text-[17px]) in package source or stories.timeline-fork— ComposeTimelineRoot/TimelineItem/Timelinefrom@elabs-ai/components-ui; never declare a localTimeline*component or hand-roll anabsolute w-pxconnector with a status-keyed style map outsidepackages/ui/src/components/timeline.type-roles— Type is a role: usetext-display-lg|display|title|subtitle|heading-xs|body|caption|meta|eyebrow|kpi|kpi-sm|code, never rawtext-sm/text-[17px]inpackages/*/src/**/*.tsx(stories are covered by thetext-scalerule).unsafe-html-justified— EverydangerouslySetInnerHTMLhas a same-line or directly-preceding comment saying why it is sanitized/trusted/escaped, and its__htmlis a sanitiser call or asanitized*/safe*value.use-controllable— A component with both controlled and uncontrolled modes (value/defaultValue,open/defaultOpen, …) usesuseControllableState(uilib/use-controllable-state.tsor Radix), never a hand-rolleduseState+x !== undefinedpair.
Themes8 rules
community-themes— Ship each downloadable theme family inthemes/<slug>/complete and readable: one[data-theme]block per<slug>-<scheme>.csswith a matchingcolor-scheme, every contract token and nothing else, AA ink pairs, no logo art (--brand-logo-*stayinitial, so brand-ui’s own mark shows and the app overrides it), and atheme.ts+ README that agree.decoration-collapse— Never let a high-decoration rule in decoration.css collapse two or more role fills (.bg-primary,.bg-success, …) to one appearance without a compensating[data-status]channel (≥2 values, same scope).decoration-css— Keepbackground-attachment: fixedinside@media (hover: hover) and (pointer: fine); mask the[data-decoration-fade]fade on an inert::beforelayer, never the host; giveoklch(from …)inks an@supports notfallback.duplicate-theme-blocks— Declare each theme selector's color tokens in exactly ONE block; a second color block wins the cascade but is invisible to every first-match tool (machinery-only blocks with no color token are fine).elevation— Use the one elevation ramp:shadow-*for resting surfaces,shadow-ring-*(no border) for floating ones,shadow-hairlinefor a bare edge — never a rawbox-shadow, an arbitraryshadow-[…], orborder+shadow-md+ in one class string.role-distinctness— Keep co-occurring roles (focus ring vs success/accent ink, current match vs destructive, every categorical chart series, adjacent sequential/mono steps, diverging steps, accent vs mono) ≥ 0.05 ΔE(OKLab) apart in every theme, after resolvingvar()aliases.surface-elevation— Keep app chrome recessed below the canvas in every theme:L(--background) − L(--sidebar) ≥ 0.02and--cardnever below--background— fix flatness in the theme's--sidebar, never in components.theme-parity— Every theme block (:rootand each[data-theme]) defines every semantic token; only:rootmachinery (--decoration*,--deco-*,--paper-*,--hairline-*,--duration-*,--t-*,--motion-*,--radius*,--font-*) is exempt.
Registry2 rules
registry-resolve— Every relative import in a registry item resolves both at its repopathand at its installtargetlayout; keeptargetfolders mirroring the repo tree.sidebar-drift— Import or re-exportTeamSwitcher,NavMain,NavUserandNavNotificationsfrom@elabs-ai/components-uiin registry sidebar blocks; never re-declare a local copy.
External commands5 rules
registry-validate— Keepregistry/registry.jsona valid shadcn registry: unique named items, valid types, real https homepage, every listed file on disk.token-contract— Keep the shipped theme token contract (THEME_TOKEN_NAMES) in sync with the theme stylesheets.tokens-fresh— Author token values in the DTCG source (packages/tokens/tokens/); the generated theme stylesheets must match a freshtokens:build.tt-aliases-resolve— The Trusted-Types alias snippet in docs/CSP-AND-NETWORK.md must resolve (in node_modules) to each package's DOM-free build.version-sync— Keep the root, plugin manifests and MCPSERVER_INFOversions equal to the fixed package group's version.