Skill / Refined UI registry
Skin authoring
Author a skin as three files with strict roles — theme.css re-values the token contract, skin.config restyles slots and declares DS choices, skin.css says only what classes cannot.
Goal
Make every pack compose with the library recipes instead of fighting them: tokens stay editor-hydratable, slot overrides merge by tailwind-merge argument order, and an app that never activates the pack pays nothing.
Registry-first guidance for installable Refined 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 `RefinedSkin` field (`sidebarLayout`, `sidebarWidth`, `indicators`, `motion`, `colorScheme`, `effects`) when the whole app should flip at once; then a `skin.config` slot; then skin.css; a shared `@utility` or an extension only after that.
2Restyle a recipe-painted property (background, border, radius, shadow — anything the component's cva/classes already set) through `skinSlot`: the config string evicts the library utility by tailwind-merge ARGUMENT ORDER, state variants included, with no cascade fight and the caller's className still winning last.
3theme.css authors every core token as a literal value (oklch/px/rem/ms — no `var()` chains) in BOTH the light and the dark block; dark must restate surface tokens or portalled popups leak the light values. Packs never open their own `@theme` block — the baseline owns it (test-enforced).
4Pick the right scoping FORMAT: an app branding itself writes tokens in its own `:root { }` / `.dark { }` (the stock shadcn convention — the baseline's `:where(:root)` zero-specificity defaults always lose to it, and the core color tokens carry shadcn's exact names). The `[data-skin="<id>"]` scoped form is the PACK format for distributable/switchable skins: the id is the design system's NAME, and the scope is what lets several packs compile in one build and portalled surfaces re-assert it — never present it as required app wiring.
5Variables 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.
6skin.css keeps to what classes cannot express: pseudo-elements, keyframes, native scrollbars, root/body inheritance, relational and state selectors, and a uniform multi-slot FAMILY (one DS concept — a popup material, a control treatment — applied to many slots by one documented rule) ONLY on properties the recipes do not already paint as utilities: where they do, the family rule silently loses and the concept belongs in a shared const spread across the config's slot entries. Everything scoped to the skin id, inside `@layer components`, with anatomy selectors carrying `[data-ui="agent"]`.
7When the element you need to restyle has no slot in `SkinSlotContexts`, add the slot (contract entry + `skinSlot(...)` in the recipe) — growing the contract is part of adding the slot, and every future skin benefits.
8A pack's behavioral fx activates through `adornments` on the component's named anchor (e.g. `"chat-input:send-layer": (ctx) => <SendAurora sendCount={ctx.sendCount} />`) — the referenced extension lives in a separate "use client" file the pack ships, the config stays RSC-pure, and the manifest lists the extension files as additional targets (liquid-metal/send-aurora precedent).
9Row-list slots (`sidebar-menu-button`, `tree-item-trigger`) may paint hover/active backgrounds freely: under `indicator="slide"` the library resets them after your classes so the pill stays the sole chrome. Keep row backgrounds on the bare or `dark:` modifier chains the reset restates (test-enforced), or take a function-form slot and branch on `ctx.indicator`.
10Run the pack test suite from apps/docs (`bun test`): theme-contract coverage, dark parity, no-@theme, fixture parity, and popover surface consistency are the enforced requirements.
Avoid
1`!important` (or the Tailwind `!` suffix) in skin.css to restyle a SINGLE slot's recipe-painted property — that is the exact cascade fight `skinSlot` eviction exists to avoid; a rule in `@layer components` without `!important` on such a property silently loses to the utility layer and ships dead.
2Painting the same slot in both skin.config and skin.css: one of the two silently wins and the other drifts unnoticed until it resurfaces as a half-applied look.
3The `!` important suffix inside skin.config classes: it does not just beat sibling utilities — it beats the caller's className that cn() puts LAST, breaking caller-wins. When tailwind-merge fails to evict a recipe shadow it is the ambiguous `shadow-[var(--x)]` form (classified as a shadow COLOR); write `shadow-(--x)` — classified as box-shadow, evicts by argument order.
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 (the ripple depends on it); disable durations through tokens, never the animation itself.
6Hardcoding geometry the token contract already parameterizes (radius, control heights): prefer the token, or a floor like `rounded-[max(3px,var(--radius-control))]` so editor knobs stay live on chrome skins.
Source
Imported as local Refined UI skill guidance, with this repo owning the final wording.
Skin contract & doctrine (file-level comment)
src/registry/skin.ts