Skip to main content

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.

Only the listed prompts run — you can edit the text, but the calls come from the prompt you picked.

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 schema

Works with your agent

Six ways in, one spec — pick your host once and every command below matches it.

Show commands for
Unit: Hosted MCPGives you: A remote MCP server — nothing to run locally.
Actions:
claude mcp add --transport http brand-ui https://elabs-ai.com/mcp
Unit: Local MCP + CLIGives you: The same tools from your machine, plus `audit --strict`.
Actions:
npx -y @elabs-ai/components-cli mcp
Unit: Claude Code pluginGives you: 9 skills your agent picks up automatically.
Actions:
/plugin marketplace add mreimitz/elabs-components
Unit: llms.txtGives you: A plain-text map of the docs for any agent that reads it.
Actions: Open
Unit: RegistryGives you: 245 shadcn-compatible blocks, copy-owned into your app.
Actions: Browse
Unit: ManifestGives you: Every export, prop and gate, machine-readable.
Actions: Open

The daily routine

Install it your way

pnpm add @elabs-ai/components-ai @elabs-ai/components-ui

Gates, not guidelines (98)

Stories8 rules
  • a11y-baseline — Axe stays blocking: preview.tsx keeps a11y: { test: "error" } and applies scripts/a11y-baseline.json, whose generated per-story exemptions never exceed ratchet.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, raw UnitStack) in a chart story, a registry/blocks/** block or a block story states its unit ("one X = N") in unitLabel, description or accessibleDescription.
  • loading-states — A component with a loading/isStreaming (or chart status: ChartStatus) prop ships a story that shows it: a *Loading/*Streaming export 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 — use w-full max-w-* or parameters.layout.
  • state-coverage — Every allowlisted stateful component (STATEFUL_COMPONENTS in 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), an ADR, an RM- roadmap item, harness or regression; that reference belongs in a comment or parameters.docs.description.story.
  • story-viewport-guard — A story meta for a layout-level surface (Layout/*, Patterns/Templates/*, Patterns/Scenarios/*, AppShell, Sidebar, DataTable, ChatShell, Hero, …) sets parameters.layout ("fullscreen"/"padded") or a viewport — never the centered default.
  • variant-coverage — Render every cva variant value in a story (variant="success" or args: { variant: "success" }); argTypes.options does not count, a default is met by a story that leaves the axis unset.
Packages24 rules
  • agent-output-contract — The manifest's agentOutput contract matches statusFromToolState (ai tool.tsx) and STATUSES (status-badge.tsx), names only real @elabs-ai/components-ai exports, and its example never calls useChat(.
  • ai-sdk-types-only — @elabs-ai/components-ai imports ai / @ai-sdk/* as types only (import type, inline type specifiers); runtime values like useChat belong 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 in scripts/attributions.sources.json (then pnpm gen) in the same change.
  • charts-definition-isolation — No chart/part/surface family imports another family's definition, and only AutoChart, definitions/components.ts and src/test/** import the definition registry — a family stays tree-shakeable on its own (ADR 0042 §11; runtime imports read with the TypeScript parser, import type exempt; test/story/fixture files exempt).
  • charts-definitions-pure — The runtime import closure of charts definitions/** and charts/props/** reaches only @elabs-ai/components-ui/definition and allow-listed pure leaves — never React/visx/d3/motion/ui at runtime (import type exempt; test/fixture files and definitions/components.ts exempt 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-ui component is ui-reuse's finding).
  • charts-test-double — Keep @elabs-ai/components-charts/test a faithful, engine-free double: export every real component from src/test/index.ts, never import @visx/d3/motion or a chart barrel at runtime from src/test/**, keep ./test in exports + publishConfig.exports + tsup, and keep /test subpaths out of the manifest.
  • dep-direction — @elabs-ai/components-* runtime deps (dependencies/peerDependencies) follow the one-way DAG tokens → ui/icons → layer-2 leaves → process; every package is registered in ALLOWED, 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 dynamic import() or a @lazy-boundary module in ai/terminal/viewer src; never import a @lazy-boundary module statically.
  • home-bundle — Keep the website's / initial JavaScript (gzip, apps/home/.next) within the home-bundle budget in scripts/check/baseline.json and free of per-section engines (@xyflow, monaco-editor, maplibre-gl, @milkdown, components-process, components-terminal); skipped without a build unless HOME_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 under apps/home/components/blocks/ keeps its // registry: <name> — copied <YYYY-MM-DD> header.
  • lucide-version — Declare ONE lucide-react version specifier across every workspace manifest (deps, peers, dev deps, pnpm.overrides, resolutions).
  • media-reuse — No raw <img>, <video> or <audio> in a package outside ui's image/ and media-player/ folders — compose Image, Video, Audio from @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 from pnpm-lock.yaml); the known, disclosed exceptions in the rule's KNOWN_DEFEATS are 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/react primitive flow wraps, no ai/maps/marketing/editor/viewer/terminal imports, and no engine or React import under src/core/; exempt one line with // process-reuse-exempt: <reason>.
  • process-test-double — Keep @elabs-ai/components-process/test complete and engine-free: re-export every double from src/test/index.ts, never import React Flow/visx/d3/motion or flow/charts/data/process barrels at runtime from src/test/**, keep ./test in exports + publishConfig.exports + tsup, and keep /test subpaths out of the manifest.
  • remote-origins — Declare every https:// origin shipped source can reach (including upstream URLs in attributions.generated.ts) in scripts/remote-origins-allowlist.json with its kind, CSP directive and escape hatch, and name it in docs/CSP-AND-NETWORK.md.
  • sanitizer-passthrough — Wrap a safe-by-default renderer (Streamdown) only with Omit<…, "rehypePlugins"> on every props type and stripSanitizerOverrides(props) before spreading props onto it; never set rehypePlugins outside the reviewed allowlist.
  • tailwind-sources — Name every package that ships Tailwind classes in each @source-bearing app CSS file with a pattern that resolves to packages/<name>/src (or node_modules/<pkg>/dist); an unlisted package renders unstyled.
  • trusted-types-patches — Keep @radix-ui/react-scroll-area and @radix-ui/react-select patched (registered in pnpm.patchedDependencies, installed dist free of HTML sinks); after a version bump re-apply the patch with pnpm 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 in docs/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-ui component — 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's capabilities.highlight lists only real address kinds, its renderer reads highlights (and rects for rect), and its *-adapter.test.tsx builds 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.txt and each llms/<pkg>.txt, a created app's brand-ui-context.md for every template, and every component's docs --brief card.
  • 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 a purpose in packages/cli/lib/intent.mjs or a docs.description.component in 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 ./test double and history docs are exempt.
  • conflict-markers — Resolve every Git merge conflict before committing: no line may start with a <<<<<<< / ======= / >>>>>>> marker.
  • csp-policy — Keep docs/csp-policy.json and the csp:published/csp:dev blocks in docs/CSP-AND-NETWORK.md §2.7 identical, and justify every non-'self' relaxation with a carve-out whose why names the reason.
  • lockfile-dup-keys — pnpm-lock.yaml never repeats a sibling mapping key (a bad merge breaks --frozen-lockfile and silently stops CI); dedupe or regenerate with pnpm 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 a biome-ignore comment (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.json and marketplace.json agree on version, every declared skill/agent path starts ./ and resolves, the brand-ui-start router is user-invocable, MCP servers are http or stdio, and shared skill docs exist exactly once.
  • pnpm-script-refs — A pnpm <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 libraries brand-ui migrate converts from are exempt.
  • test-concurrency — The root test script runs turbo run test --concurrency=<int>; never raise a vitest testTimeout to absorb CPU oversubscription (#80).
  • tt-aliases — Every app aliases decode-named-character-reference and hast-util-from-html-isomorphic via require.resolve(…), declares both as direct devDependencies, and docs/CSP-AND-NETWORK.md still documents both.
Components37 rules
  • ai-microcopy-a11y — Every literal aria-label and placeholder in @elabs-ai/components-ai goes through t() — 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-grid at full opacity and CHART_HAIRLINE_WIDTH (never a dimming opacity/strokeOpacity or another numeric strokeWidth), and never alias --chart-grid to var(--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_DASH in chart-stroke.ts, chart-opacity.ts, animation.ts) instead of literals; the count of literals left in packages/charts/src only 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 (zoom on 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 by sqrt(value / max), and never call Math.random (use seededRnd); a reasoned exception carries // honesty:allow <reason>.
  • charts-responsive — Every @elabs-ai/components-charts container publishes its breakpoint tier (renders ChartPlotRoot, sets data-chart-breakpoint, or wraps a container that does), and a Responsive<…> prop is read only through resolveResponsive / useResponsiveValue or handed on whole — never branched on directly (ADR 0039); opt out with // charts-responsive-exempt: <reason>.
  • collapse-fork — Build a collapsing side panel on useCollapsiblePanel (@elabs-ai/components-ui); never hand-roll a transition-[…width…] tween plus an off-screen [calc(var(--x)*-1)] slide outside packages/ui/src/components/collapsible-panel.
  • component-registration — Every @elabs-ai/components-ui component folder is re-exported from src/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 root data-slot="<kebab-name>" and each sub-part data-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-50 plus disabled:pointer-events-none (Button) or disabled:cursor-not-allowed (Input) in the same class list.
  • effect-cleanup — A useEffect/useLayoutEffect that 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 use focus-ring/focus-ring-within/focus-ring-inset/focus-ring-static, never a hand-rolled focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring stack.
  • forward-ref-required — An exported component that spreads ...props onto a DOM element is wrapped in forwardRef.
  • header-band — Size every header band (<header>, or a data-slot ending -header/top-bar, with a border-b) with h-header, never h-14/h-12 or 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 arbitrary duration-[…]/ease-[…] in apps/home/**/*.{ts,tsx,css}; app/globals.css may only reference var(--…).
  • i18n-strings — UI text in package source (JSX text, aria-label/title/placeholder literals) 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 with Intl.* and a locale prop: no toLocaleString()/toLocaleDateString()/toLocaleTimeString() without a locale, no new Date(…).toString() in JSX.
  • logical-props — Use logical direction utilities (ms-/me-/ps-/pe-/start-/end-/border-s/rounded-s/text-start), never physical ml-/pr-/left-/border-l/text-right, so layouts mirror in RTL.
  • memo-on-children — Never wrap a component whose props include children in memo/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 seam t() (ADR 0017); a genuinely untranslatable string carries // i18n-exempt: <reason>.
  • microtypography-apostrophe — Write the curly apostrophe ’ (never a straight ' between letters) in JSX text and aria-label/placeholder/title/description values, stories included; opt out with // microtypography-exempt: <reason>.
  • microtypography-ellipsis — Write "…" (U+2026), never "...", in JSX text and aria-label/placeholder/title/description values, 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|slower and ease-standard|entrance|exit, never duration-<N>, ease-in/ease-out/ease-in-out or transition-all (docs/MOTION_GUIDELINES.md).
  • no-fixed-trigger-width — Triggers (*Trigger components, data-slot="*-trigger") size with min-w-*/w-full, never a fixed w-40/w-[180px].
  • no-index-key-reorderable — List keys are stable ids from the item, never the .map index (key={i}); placeholder lists (Array.from({ length }), (_, i)) are exempt.
  • radius-rungs — Radius comes from the rounded-* scale (backed by --radius), never an arbitrary rounded-[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 with BUILT_IN_THEMES, in package source comments and stories.
  • status-rung — Paint running status text with the ink rung text-<tone>-text; a bare text-<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 bare border in 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 and border bg-card are fine.
  • text-scale — Set type with a role (text-display-lg|display|title|subtitle|heading-xs|body|caption|meta|eyebrow|kpi|kpi-sm|code or <Heading>/<Text>), never a raw size utility (text-sm, text-xl, text-[17px]) in package source or stories.
  • timeline-fork — Compose TimelineRoot/TimelineItem/Timeline from @elabs-ai/components-ui; never declare a local Timeline* component or hand-roll an absolute w-px connector with a status-keyed style map outside packages/ui/src/components/timeline.
  • type-roles — Type is a role: use text-display-lg|display|title|subtitle|heading-xs|body|caption|meta|eyebrow|kpi|kpi-sm|code, never raw text-sm/text-[17px] in packages/*/src/**/*.tsx (stories are covered by the text-scale rule).
  • unsafe-html-justified — Every dangerouslySetInnerHTML has a same-line or directly-preceding comment saying why it is sanitized/trusted/escaped, and its __html is a sanitiser call or a sanitized*/safe* value.
  • use-controllable — A component with both controlled and uncontrolled modes (value/defaultValue, open/defaultOpen, …) uses useControllableState (ui lib/use-controllable-state.ts or Radix), never a hand-rolled useState + x !== undefined pair.
Themes8 rules
  • community-themes — Ship each downloadable theme family in themes/<slug>/ complete and readable: one [data-theme] block per <slug>-<scheme>.css with a matching color-scheme, every contract token and nothing else, AA ink pairs, no logo art (--brand-logo-* stay initial, so brand-ui’s own mark shows and the app overrides it), and a theme.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 — Keep background-attachment: fixed inside @media (hover: hover) and (pointer: fine); mask the [data-decoration-fade] fade on an inert ::before layer, never the host; give oklch(from …) inks an @supports not fallback.
  • 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-hairline for a bare edge — never a raw box-shadow, an arbitrary shadow-[…], or border + 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 resolving var() aliases.
  • surface-elevation — Keep app chrome recessed below the canvas in every theme: L(--background) − L(--sidebar) ≥ 0.02 and --card never below --background — fix flatness in the theme's --sidebar, never in components.
  • theme-parity — Every theme block (:root and each [data-theme]) defines every semantic token; only :root machinery (--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 repo path and at its install target layout; keep target folders mirroring the repo tree.
  • sidebar-drift — Import or re-export TeamSwitcher, NavMain, NavUser and NavNotifications from @elabs-ai/components-ui in registry sidebar blocks; never re-declare a local copy.
External commands5 rules
  • registry-validate — Keep registry/registry.json a 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 fresh tokens: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 MCP SERVER_INFO versions equal to the fixed package group's version.
Each gate’s rule source is in docs/GATES.md on GitHub.