Architecture
A concise map of the registry, skins, runner examples, hooks, and blocks.
The layers
The public agent surface is one clean TSX file. Hooks hold local UI behavior, skins own markup and styling, usage examples map external message shapes, and blocks compose multiple agent surfaces together.
components/refined-ui/*.tsx is the public surface users import.components/refined-ui/hooks/use-*.ts stores local UI behavior that should not be duplicated by skins.ComponentProps<> and forward ...props.components/refined-ui/blocks/* contains complete examples that compose the public agent surfaces.Runtime-agnostic UI
Refined UI surfaces should render from plain props and children. Context is allowed for compound UI anatomy, such as role, density, selected action, copy value, or open state; it must not become a runtime provider that owns messages, threads, model calls, runs, or transport.
import { ChatMessage, ChatMessageAvatar, ChatMessageBody, ChatMessageContent, ChatMessageHeader, ChatMessageRow,} from "@/components/refined-ui/chat-message";export function AssistantMessage({ children }: { children: ReactNode }) { return ( <ChatMessage from="assistant"> <ChatMessageRow> <ChatMessageAvatar>AI</ChatMessageAvatar> <ChatMessageBody> <ChatMessageHeader>Assistant</ChatMessageHeader> <ChatMessageContent>{children}</ChatMessageContent> </ChatMessageBody> </ChatMessageRow> </ChatMessage> );}One library, skins as data
ONE library ships; a SKIN is a look applied globally over it: theme skins carry tokens only, advanced skins add a typed
per-slot class config plus a skin.css layer and optional decorative adornments. A skin is data layered on the one
library, never a source fork - and shadcn compatibility is a contract the library keeps (shared core token names,
shadcn-shaped APIs), not a second source tree.
skin.css, and adornments.data-ui, data-slot, data-control, data-variant, data-tone, and data-size.cn(library recipe, skin slot override, caller className) so the skin can replace conflicting utilities without specificity wars.RefinedSkin fields - sidebarLayout, sidebarWidth, indicators, motion, colorScheme - resolved at render; an explicit prop always wins.motion: "reduced", which stamps data-motion and collapses --duration-* to 0.The customization ladder
Every "make it look/behave different" request lands on exactly one of eight rungs. Climb from the top and stop at the first rung that suffices: each step down costs more and is harder to undo. Rungs one to seven never touch library files, so library updates stay merge-free; the eighth exists because this is a registry and the source is yours. For each rung: what it is, when it is the right one, what it costs an app that never uses it, and a real example from this repo.
theme.css) - re-value a name the token contract already exposes. In YOUR app this is a plain :root { } / .dark { } block in your own CSS - the stock shadcn convention, and the core color tokens carry shadcn's exact names - because the baseline authors its defaults at zero specificity (:where(:root)), so any value you write wins. The [data-skin="id"] scoped form is the PACK format: it exists so several distributable skins can compile in one build and portalled surfaces can re-assert their scope - reach for it when you ship or switch skins, not for the one brand your app always wears. Right whenever the change is a value the contract names: a color, a radius, a type rung, a shadow knob, a duration. Costs nothing when untouched - zero JS, zero markup, the baseline defaults hold. Example: a compact skin can square the UI with --radius: 0 and repaint the palette; every pack re-values --text-body or --primary this way.Button variant="quiet" next to variant="outline" on the same screen. If an app would only ever pick one of the two, it is NOT a variant - it is a design-system choice (next rung). Tabs is the precedent: segmented vs line is one-per-app, so it is a container attribute (data-tabs="line"), not a prop. Cost: a variant is contract forever - every skin's slot config sees it in ctx and must react to it.RefinedSkin field resolved at render when the component must render differently (sidebarLayout, sidebarWidth, indicators, motion, colorScheme, effects), or a CSS-only container attribute when it is pure paint (data-tabs="line"). An explicit prop still wins - caller-wins is the shadcn contract. Costs nothing when undefined: app defaults hold, and indicators left at none means the highlight engine's lazy chunk is never downloaded. Examples: cuicui sets sidebarWidth: "20rem", modern-apple sets sidebarLayout: "floating", the docs nav opts into the sliding pill per-instance with indicator="slide".skin.config) - one skin REACTING to the existing variants with per-slot classes, merged by tailwind-merge argument order. Right when a pack needs to restyle what the recipe already paints, or apply its own @utility. It never adds a variant or prop. Costs nothing when untouched: an unconfigured slot resolves undefined and the module stays RSC-pure. Example: xp's button slot swaps in xp-bevel-up per ctx.variant.skin.css - ONLY what classes and tokens cannot say: pseudo-elements, keyframes, native scrollbars, descendant families, and the pack's own invented variables (a chamfer, a Luna gray). Anti-patterns: re-styling a slot that skin.config can already reach, and re-implementing a library recipe - re-value the recipe's component-local variables instead. Costs nothing when absent; scoped [data-skin], in @layer components.effects.css) - a reusable, token-parameterized effect shared across skins: shimmer, scroll-fade, and friends are single @utility definitions that read theme tokens, so each pack re-values the tokens and never ships a prefixed copy. Right when several skins want the same effect with different parameters. Costs nothing when unused - Tailwind only emits a @utility referenced in scanned source.data-effects ancestor, so a skin-declared ripple follows a Button into a Dialog. ANCHORED for component-scoped fx: the component ships a named adornment anchor and skin.config fills it with the extension (send-aurora on chat-input:send-layer). Cost at the install boundary: zero either way - not installed means no import, no bytes.Extensions, anchors, and integrated behaviors
An EXTENSION is an optional installable layered on the library - never part of a component's bundle, zero cost when
absent. Two attachment modes share the concept. ROOT-MOUNTED extensions are mounted once above their targets and
discover them through the emitted anatomy; no component imports them (control-effects, view-transition). ANCHORED
extensions fill a named adornment anchor a component ships: the component owns a positioned wrapper (aria-hidden,
pointer-events-none, paint containment) that renders zero DOM until skin.config fills it - the send-aurora layer on
ChatInput's chat-input:send-layer anchor is the reference. An INTEGRATED BEHAVIOR is the third, different thing: part
of a component, imported by it, switched on by a design-system choice - the sliding selection pill (track-highlight)
is imported by Sidebar, Tree, and TableOfContents on purpose; calling it an extension would be false.
data-state/data-slot anatomy, no mechanism at all; a transient EVENT a skin should own (a send) → an anchored extension; per-instance decoration → composition at the call site; cross-cutting uniformity (all controls ripple or none) → a root extension; behavior needing component internals that many skins want → an integrated behavior; everything else → edit your installed source (rung 8).sendCount), the extension keys its layer on it, and a mount-time keyframe (or @starting-style) plays once and parks invisible - durations ride --duration-*, so the motion kill-switch collapses it for free.skin.config ONLY - an installed pack or your own app brand, same gesture: adornments: { "chat-input:send-layer": (ctx) => <SendAurora sendCount={ctx.sendCount} /> }. The referenced extension is a separate "use client" file, so the config stays RSC-pure. Import boundary = install boundary: no skin fills the anchor, no bytes ship.[data-ui="agent"][data-control="true"] selectors, write DOM and custom properties outside the React tree, tear down cleanly. A runtime mounted while its skin is inactive parks: liquid-metal gates its body-wide MutationObserver on the active skin.skin.config never MOUNTS a root runtime - a pack documents its runtime and the app mounts it once in its layout (LiquidMetalSkinRuntime in app/layout.tsx). Filling an adornment anchor is the sanctioned skin.config channel; mounting runtimes is not.RefinedSkin.indicators or an explicit indicator prop), and lazy() keeps them free - with the default none, the track-highlight chunk is never downloaded. An anchored extension that becomes universal is PROMOTED to this tier.{/* fx anchor candidate: "chat-message:stream-layer" */}): it documents the surface and the promotion path - it is never an activation channel, because a pack cannot fill a comment and hand-rolled placement loses the wrapper's guarantees.Versions of a name vs versions of a usage
Should a component ship alternative versions - a "sidebar v2", a "compact chat-input"? Two contents under ONE registry name: never - that is two sources of truth, the most expensive thing this registry can add. But one USAGE genuinely served by several renderings is real, and it has a cheap shape: sibling registry items sharing one export and one props contract, grouped on one docs page with a version picker. The consumer installs exactly one.
RefinedSkin field. None of them fork the component.audio-visualizer (bars) and audio-visualizer-line - both export AudioVisualizer against the same AudioVisualizerProps from contracts.ts, so swapping versions is an import-path change and no call site moves. Distinct file targets, distinct manifests: the registry never holds two truths of one name.The skin config file
skin.config.tsx is the one user-owned file every slot reads through skin.ts: a static module exporting the active skin.
Installing a skin pack replaces this file and adds the pack's CSS - no provider to mount, no component to wrap, no library
file to reopen.
{ id: "refined" } - the library default costs nothing and needs nothing.adornments fills a component's named anchors with decorative JSX or (ctx) => JSX for behavioral anchors - a pack ships its fx there, and your own brand uses the exact same field. See the extensions section.skin.config.tsx is getter-based and the theme editor swaps the active skin for the whole site.Composition choices
Base UI-backed slots should prefer Base UI's render and nativeButton composition path. asChild remains a compatibility
bridge for shadcn/Radix-style ergonomics, not the recommended path for new Base UI-backed slots.
render keeps primitive behavior, refs, aria, and event merging in the underlying headless primitive.nativeButton declares whether the rendered element is still a real button, so links and divs keep correct semantics.control-content are allowed when they create a reusable layering contract for skins and extensions.data-slot, and documented.Registry source of truth
The manifest is a delivery mechanism, not the docs content. The source tabs read real files, and validation keeps public
registry JSON mirrored to /r.
components/refined-ui/chat-message.tsxcomponents/refined-ui/hooks/use-chat-message.tscomponents/refined-ui/blocks/chat.tsxcomponents/ui/button.tsx