Get started
Choose a skin, install a component or complete block, wire its CSS, and compose your application runtime.
Choose what to install
Pick one skin, then install components one by one or a block as a complete recipe. The skin is required: it owns every token value the components paint with.
Starting fresh? Each skin page exposes an all-<skin> manifest that installs the complete component set plus that
skin in one command.
- 1Chooseskin + UI surface→
- 2Install skincomplete token owner→
- 3Install UIcomponent or block→
- 4Wire CSScore + active skin→
- 5Map dataplain props + children→
- 6Own itedit local source
Install a skin
npx shadcn@latest add https://control-ui.dev/r/skin-refined.json --overwriteExactly one pack owns skin.config.tsx, skin-theme.css, and skin.css. --overwrite claims all three — without
it the CLI keeps the previous pack's files in place.
The current skin contract is version 7: registered component CSS knobs carry every visual decision, published in
skin-contract.json. Upgrade notes for earlier versions are in the update section below.
Install a component
npx shadcn@latest add https://control-ui.dev/r/chat-message.jsonInstall a block
The chat block is a complete composition: messages, input, attachments, markdown, composer controls, and actions.
npx shadcn@latest add https://control-ui.dev/r/chat-block.jsonWire the CSS
Each item adds its own imports to the CSS entry named in components.json. Core plus a skin leaves you with:
/* app/globals.css */@import "tailwindcss";@import "../components/control-ui/styles/theme.css";@import "../components/control-ui/styles/skin-theme.css";@import "../components/control-ui/styles/skin.css";The registry always writes ../components/…. When your entry sits elsewhere — app/globals.css with @/* pointing
at ./src/* — run node <install dir>/scripts/fix-css-imports.mjs: it rewrites every import to the real path.
Stamp the pack id on the root element. There is no token fallback when data-skin is missing or misspelled:
<html data-skin="refined">…</html>Compiled styles assume Safari 16.4+, Chrome 119+, Firefox 128+; the architecture guide's cascade section derives the floor.
Compose your runtime
Render provider-owned messages directly with Control UI components. Keep native provider types — no intermediary message schema.
import type { MastraDBMessage } from "@mastra/core/agent/message-list";import { MessageFactory, type MessageRoleRendererProps, type MessageRoleRenderers } from "@mastra/react";import { ChatMessage, ChatMessageBody, ChatMessageContent, ChatMessageRow } from "@/components/control-ui/chat-message";function MessageFrame({ from, children }: MessageRoleRendererProps & { from: "user" | "assistant" | "system" }) { return ( <ChatMessage from={from}> <ChatMessageRow> <ChatMessageBody> <ChatMessageContent>{children}</ChatMessageContent> </ChatMessageBody> </ChatMessageRow> </ChatMessage> );}const roles = { User: (props: MessageRoleRendererProps) => <MessageFrame {...props} from="user" />, Assistant: (props: MessageRoleRendererProps) => <MessageFrame {...props} from="assistant" />, System: (props: MessageRoleRendererProps) => <MessageFrame {...props} from="system" />, Signal: () => null,} satisfies MessageRoleRenderers;export function Message({ message }: { message: MastraDBMessage }) { return ( <MessageFactory message={message} roles={roles} Text={({ text }) => <span>{text}</span>} fallback={(part) => <span>Unsupported message part: {part.type}</span>} /> );}Update installed components
# Preview upstream changes against your installed sourcesnpx shadcn@latest add https://control-ui.dev/r/update.json --diff# Refresh every installed source; skin files stay untouchednpx shadcn@latest add https://control-ui.dev/r/update.json --overwriteUpdating is a reinstall. The update manifest refreshes every installed source and never touches the three
skin-owned files — a fully custom skin included. Installed source is owned but never edited; customization lives in
the skin.
--diff previews per-file what the overwrite would change.update pulls the whole set.npx shadcn@latest add …/r/skin-<id>.json --overwrite.all-<skin> with --overwrite also updates everything, but resets the three skin files to that pack.node <install dir>/scripts/fix-css-imports.mjs heals the entry imports, and node <install dir>/scripts/control-ui-doctor.mjs audits the wiring — imports, theme order, app @theme conflicts, data-skin..claude/skills/control-ui/SKILL.md, the agent skill installed with the set.Crossing skin contract versions reinstalls core, the affected components or their blocks, and the skin together.
Version 4 replaced ToolCall with Activity's typed tool variant; from version 3 or earlier, remove both old layouts
before reinstalling:
rm -f components/control-ui/tool-call.tsx components/control-ui/hooks/use-tool-call.tsrm -f src/components/control-ui/tool-call.tsx src/components/control-ui/hooks/use-tool-call.tsThen replace ToolCall with <Activity kind="tool" name="…"> and its Activity* parts — ActivityDetail,
ActivityDetailLabel, ActivityDetailContent — mapping provider status directly to ActivityState. Drop
useToolCall; no legacy selectors ship.