Create a skin
Re-value the token contract over an installed pack, or own a full pack of three files, then reach the component knobs beneath.
Start with the defaults
Control UI renders without a skin. Core owns a complete neutral token baseline in light and dark modes. Your application owns its theme: override shared tokens, then use component knobs for specific treatments.
Re-value application tokens
Override tokens in your own CSS after the library imports. With no provider, root tokens also reach portalled surfaces.
:root { --primary: oklch(0.45 0.18 260); --radius: 4px;}.dark { --primary: oklch(0.78 0.1 260);}These overrides survive component updates because they belong to your application. You only need to declare the values that differ from the library defaults.
Retint a color ramp
Neutral, primary, and seven hues each derive a twelve-step ramp from one seed. Steps keep their job in both modes, so a seed needs one declaration, not a light and a dark pair. Every role that aliases the ramp follows: retinting red repaints red badges, and retinting green repaints success text.
:root { --scale-red-seed: oklch(0.6 0.2 18); --scale-neutral-seed: oklch(0.5 0.01 250);}Override a role instead when one surface should leave its ramp: --badge-red-foreground or --success-text.
Recipes read roles only, so steps are yours to reference in application CSS. See every ramp and role live on
Foundations.
Add an optional skin
A named skin can own skin-theme.css for tokens, skin.css for component knobs, and skin.config.tsx for behavior
choices and adornments. Refined and the other registry presets are examples you can install and edit.
npx shadcn@latest add https://control-ui.dev/r/skin-refined.json --overwriteImport the installed skin-theme.css and skin.css after core and recipe CSS. Supply the config through the provider
in a client module, so configurations containing render functions remain inside the client boundary:
"use client";import type { ReactNode } from "react";import { SkinProvider } from "@/components/control-ui/skin-provider";import { skin } from "@/components/control-ui/skin.config";export function AppTheme({ children }: { children: ReactNode }) { return <SkinProvider skin={skin}><div data-skin={skin.id}>{children}</div></SkinProvider>;}The CSS selector and provider id must agree. A portalled popover, dialog, or menu receives that same id. Each provider scopes its own subtree; omitting a provider uses the neutral defaults and root theme.
Set scrollAreaScrollbarVisibility: "always" and scrollAreaScrollbarGutter: "stable" in the skin config for
persistent scrollbar tracks that reserve layout space, as in Windows XP. ScrollArea's scrollbarVisibility and
scrollbarGutter props override these defaults for individual instances. The neutral defaults are "hover" and
"auto", which use overlay scrollbars.
The example packs resolve the required token contract explicitly so each has a complete, independent appearance. A consumer theme may inherit defaults and override only the values it needs.
Go deeper with component knobs
When shared tokens cannot express a treatment, re-value the registered --cui-* knobs in your CSS.
[data-skin="acme"] :where([data-control-family="button"][data-control="true"]) { --cui-button-radius: 999px; --cui-button-hover-background: oklch(from var(--primary) l c h / 0.12);}Every knob's name, syntax, initial value, and recipe default appear in its family's /r/contract/ file.
Re-value knobs at family roots so their parts inherit the treatment.
Browser tabs and their connected content share --cui-tabs-surface-background, --cui-tabs-surface-radius, and the
tabs border knobs. Use TabsSurface around content when wrappers separate the list from its panels, and embed a
Code with chrome="embedded" so it uses that surface. --cui-tabs-indicator-background styles segmented tabs;
changing it does not split the connected browser surface.
Nested buttons and fields derive their default corner from the containing surface's radius, border, and padding.
Their --cui-button-radius and --cui-field-radius knobs remain authoritative: setting one explicitly overrides
that fitted default, including inside a composer. A circle-shaped button also keeps its circular shape.
When a skin changes its control default, use var(--nest-radius, <skin-radius>) as the knob value to retain the
container's fitted corner inside composed controls.
The composer uses --radius-composer and --composer-padding; --radius-field controls message bubbles.