Install
One command installs the complete set and its skin as source you own. The CSS entry is the only wiring you write.
Install the components
One command copies every component, block, and primitive into your repository as source you own. Core includes neutral defaults for both light and dark modes. No skin, configuration file, or provider is required.
npx shadcn@latest add https://control-ui.dev/r/all.jsonA components/control-ui/ directory holds the source and styles/theme.css holds the library defaults.
Install one item at a time
Install just the components you need. Each item brings its dependencies and the same core defaults.
npx shadcn@latest add https://control-ui.dev/r/chat-message.jsonBlocks bring a complete composition of the same components.
npx shadcn@latest add https://control-ui.dev/r/chat-block.jsonWire the CSS
Each item appends its own imports to the CSS entry named in components.json. For a button:
@import "tailwindcss";@import "../components/control-ui/styles/theme.css";@import "../components/control-ui/styles/recipes/button.css";A control renders with neutral colors and square corners. An opened popover or dialog receives the same root tokens.
The registry writes ../components/…. If your aliases use different paths, run
node <install dir>/scripts/fix-css-imports.mjs, then verify below.
Verify the install
The doctor ships with the install. It is read-only: it reads the CSS entry named in components.json, follows every
import it finds, and looks for the data-skin stamp.
node <install dir>/scripts/control-ui-doctor.mjsKeep it runnable as a script, so a later session — or your agent — can check the wiring without this page:
{ "scripts": { "control-ui:doctor": "node components/control-ui/scripts/control-ui-doctor.mjs" }}$ npm run control-ui:doctorcontrol-ui doctor: clean.Anything else is one of the five checks below. Errors exit non-zero and block: styling on top of them is what reads as a broken install afterwards. Warnings exit zero and are deletion offers, named file by file and token by token.
../components/… and your entry sits somewhere else. Run node <install dir>/scripts/fix-css-imports.mjs; one wrong prefix silently unstyles everything below it.*.control-ui-theme.css composes over the pack, so source order is what makes it win. The same script repairs the order.@theme block redeclaring Control UI keys. Tailwind merges every @theme in the build and the last declaration wins, so the block detaches those utilities from the skin — a hardcoded font splits the app into two typefaces, a redeclared --radius collapses every corner.:root or .dark block declaring skin token names. The skin out-specifies it, so those declarations are dead; a hybrid block keeps the names the skin never declares painting a second palette beside it.data-skin stamp. An optional installed preset needs a matching scope and provider; without a preset, the library defaults work with no stamp.Two flags reach beyond the wiring audit. --contrast resolves the theme contract's required color pairs straight
from the installed CSS — no browser, no network — and prints a per-mode tally with every failure named:
$ node components/control-ui/scripts/control-ui-doctor.mjs --contrastlight: 55/55 passdark: 55/55 passIt reads the Tailwind palette, the core theme, the installed skin theme and every *.control-ui-theme.css the
entry imports, in that order, so it sees exactly what the browser resolves. A pair a recipe paints — popup items,
sidebar, tabs, solid buttons, tooltip, focus ring, badges — is not guessed at; those live in the rendered audit at
/theme-accessibility. A failure names the pair, its ratio and the threshold, and exits
non-zero. A value the engine cannot model, such as color-mix(), is reported as unverified instead of passing.
--emit-css turns a theme artifact into the one derived stylesheet an app imports:
$ node components/control-ui/scripts/control-ui-doctor.mjs --emit-css midnight.control-ui-theme.jsonIt writes midnight.control-ui-theme.css beside the artifact. Never hand-write those selectors: the emitted scope
doubles the data-skin attribute to match the pack's own weight, which is what hands the win to source order.
The doctor audits wiring, never taste. It cannot see a token that is ugly, only one that is undeclared, dead, or fighting the skin. Each deletion it offers is a change to source you own, so it names the file and the token instead of writing them. Run it after every install, update, and migration.
Own your theme
Re-value tokens in your application's CSS. Core declares defaults at zero specificity, so your declarations win. “No skin” in these docs shows that library baseline.
:root { --primary: oklch(0.45 0.18 260); --radius: 4px;}Refined and the other example skins are optional registry presets. Installing one adds its token CSS, component
CSS, and config. Pass that config to SkinProvider and use its id on your theme boundary. The provider carries
the id and behavior choices through portals. See the create-a-skin guide for the complete setup.
Registered component knobs are published by paint family under
/r/contract/, indexed by /r/contract/index.json.
Or install the package
The same components and neutral defaults ship as @ctrl-ui/react. The package contains no skin presets.
Import paths mirror the registry tree: replace @/components/control-ui/ with @ctrl-ui/react/.
bun add @ctrl-ui/react@import "tailwindcss";@import "@ctrl-ui/react/styles/index.css";@source "../node_modules/@ctrl-ui/react/dist";Use components immediately, then add your application theme. SkinProvider is optional in both install paths.