Skill / Control UI registry
Skin authoring
Author a skin as three files with strict roles — theme.css owns shared tokens, skin.css re-values registered component knobs, and skin.config declares behavior and adornments.
Goal
Make every pack self-contained and composable with typed CSS knobs, complete theme tokens, scoped CSS, and no runtime visual class resolver.
Registry-first guidance for installable Control UI components, skins, hooks, and blocks.
Checks
1Start every visual change at the lowest rung of the customization ladder (/architecture#customization-ladder): a contract token in theme.css first; then a DS-level `ControlUiSkin` field (`sidebarLayout`, `sidebarWidth`, `indicators`, `motion`, `colorScheme`, `effects`) when the whole app should flip at once; then a registered component knob re-valued in skin.css; then CSS-only rules or an extension.
2Recipe CSS owns every component visual default and paints only through registered `--<family>-*` custom properties. Variants, tones, states, and anatomy selectors re-value knobs instead of repeating paint declarations.
3Skin CSS re-values recipe knobs under literal `[data-skin="<id>"] :where([data-control-family="<family>"][data-slot="<part>"])` selectors. Keep paint in the recipe so one override survives every component state.
4Every public knob appears once in the family contract array, has a typed `@property`, and receives a recipe default on the element that reads it. Derive the component's `KnobStyle` type from that array and expose it through `style?: CSSProperties & <Family>KnobStyle`.
5theme.css resolves every contract token in BOTH light and dark, either through a shared declaration or explicit mode declarations; skin-local formulas are allowed, but missing values are not. Packs never open their own `@theme` block — core owns bindings, neutral token defaults, and invariant mechanics.
6Scope every skin under `[data-skin="<id>"]` and stamp exactly one valid id on the application root. The id is the design system's name, lets several packs compile in one build, and is re-asserted on portalled surfaces. Pass the config to SkinProvider so portals receive the same id. Without a provider, components use core defaults and application root tokens.
7Variables the pack invents (`--<pack>-*`) are declared in skin.css beside the rules that read them, scoped `[data-skin="<id>"]` — never added to theme.css and never leaked unscoped.
8Use descendant rules for coordinated anatomy and semantic families, but change recipe-painted properties by re-valuing their knobs rather than competing with the recipe declaration.
9When a visual decision has no knob, add the typed property, recipe default, contract entry, and skin override together. Per-instance knob values travel through the typed `style` prop and remain last in the cascade.
10A pack's behavioral fx activates through nested `adornments` on the component's named anchor (e.g. `adornments: { "chat-composer": { "send-layer": (ctx) => <SendAurora sendCount={ctx.sendCount} /> } }`) — the referenced extension lives in a separate "use client" file the pack ships, the config is imported by a client provider wrapper, and the manifest lists the extension files as additional targets.
11Run the pack test suite from apps/docs (`bun test`): knob completeness, typed registrations, fixture parity, theme coverage, and popup surface consistency are enforced requirements.
Avoid
1`!important` or the Tailwind `!` suffix to beat a recipe-painted property — add or re-value the corresponding knob instead.
2Painting a recipe-owned background, border, radius, shadow, color, typography, or motion property directly in skin.css; parallel paint declarations drift as component states evolve.
3Unregistered component custom properties, knobs missing from the contract array, or contract entries without a recipe default.
4Bare host-element selectors (`button`, `input`, `code`) or un-layered rules — they leak past the component anatomy and outlive the skin's scope.
5`animation: none` for calm variants — the motion kill-switch collapses `--duration-*` to 0ms so `animationend` cleanup still fires; disable durations through tokens, never the animation itself.
6Hardcoding geometry the token contract already parameterizes; prefer the shared token or a component knob so editor controls and skins stay live.
Source
Imported as local Control UI skill guidance, with this repo owning the final wording.
CSS knob architecture
src/registry/sources/control-ui/recipes/button.css