Refined UI

An opinionated, customizable superset of shadcn/ui

AgentsPrimitivesSkillsSkins
Guides
  • Overview
  • Get started
  • Shadcn compatibility
  • Architecture
  • Agent surface
Agents
  • Action Bar
  • Audio Recorder
  • Audio VisualizerBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Chat Input
  • Chat Input Attachment
  • Chat Message
  • Chat Scene
  • Code Block Editor
  • Dynamic NotificationExpExperimental — the contract and the rendering can change without notice. Own the installed copy before shipping it.
  • Environment Variables
  • Inline Attachment
  • Markdown Block
  • Task List
  • Thread Rail
  • Tool Call
  • User Ask
Blocks
  • Chat
  • Theme toggle
  1. Guides
  2. Architecture
Guide

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.
Usage examples map provider data into shared contracts without becoming required runtime code.
Compound parts are typed with 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.

The app maps any runtime shape into Refined UI props.
A message component can own row layout, avatar placement, alignment, header, footer, and visible content slots.
The runtime stays outside: submit handlers, streaming, persistence, branching, and tool execution belong to the host app.
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 axis: theme skins are tokens only; advanced skins add slot classes, skin.css, and adornments.
Refined primitives emit stable data anatomy: data-ui, data-slot, data-control, data-variant, data-tone, and data-size.
Every slot composes cn(library recipe, skin slot override, caller className) so the skin can replace conflicting utilities without specificity wars.
A skin config reacts to existing variants through a typed context and can return decorative adornments; it never adds variants or props.
A skin also carries design-system decisions as typed RefinedSkin fields - sidebarLayout, sidebarWidth, indicators, motion, colorScheme - resolved at render; an explicit prop always wins.
Resolution is a plain function over a static config module: no hook, no context, no provider.
A skin may set motion: "reduced", which stamps data-motion and collapses --duration-* to 0.
The Skins section gives each pack its own page: theme, skin CSS, config source, install command, and how-to-build guidance.

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.

1. Token (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.
2. Variant (prop) - a new prop value on a component. STRICT rule: a variant exists only when both values legitimately coexist inside ONE app at different call sites, like 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.
3. Design-system choice (skin-level) - decided once per DS, not per call site. Two carriers: a typed 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".
4. Slot (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.
5. 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.
6. Global utility (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.
7. Extension - an optional installable layered on the library, in one of two attachment modes (see the extensions section below). ROOT-MOUNTED for cross-cutting behavior CSS cannot express: mounted once, scoped by an anatomy attribute, never imported by a component - the control-effects runtime delegates one document-level listener and resolves its scope from the nearest 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.
8. Edit the installed source - the sanctioned escape hatch for restructuring anatomy; conscious, local, last resort. It is a registry: the file is yours.

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.

Sort every fx request with one rule set: persistent STATE (streaming, open, focus) → plain CSS on the emitted 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).
An anchor is ONE position contract: the component decides where the layer lives, its stacking, clipping, hit-testing, and a11y - the skin only supplies visuals. Anchors have behavioral ctx as plain render-time values (a render prop, NOT React Context), are rare, and are justified like slots. Never a portal: the layer renders inline in the component's own stacking context.
One-shot fx replay is structural, not stateful choreography: the anchor ctx carries a counter (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.
Activation lives in 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.
Root extensions attach by anatomy, not by import: mount once, delegate events, target [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.
Integrated behaviors are the opposite trade: the component imports them, a DS choice activates them (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.
Components without an anchor carry a breadcrumb comment with the canonical slug ({/* 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.
SSR-safe by construction, all kinds: loaded lazily on the client over an already-correct first paint.

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.

A different look is a skin; a different value is a token; a per-DS behavior is an RefinedSkin field. None of them fork the component.
A different INTERACTION PARADIGM is a NEW component with its own name: thread-rail exists beside sidebar rather than as a sidebar version. Two named components can share contracts; two versions of one name cannot coexist in an install.
USAGE VERSIONS are the sanctioned middle: audio-visualizer ships as 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 docs page carries the family: one entry, a version picker that swaps preview, install command, and source together - the page always shows exactly what the chosen item installs.

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.

Native usage stays clean: render Button, Select, Menu, Tabs, and agent surfaces with their normal props.
The shipped default is { id: "refined" } - the library default costs nothing and needs nothing.
Portal-rendered popups re-assert the active skin id on their positioner, so menus and dialogs stay themed outside any token-scoped ancestor.
The config is read at render time, so dynamism is opt-in per app: export getters for theme switchers or per-tenant branding.
The config is ALSO where anchored extensions activate: 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.
The docs gallery dogfoods this: its own skin.config.tsx is getter-based and the theme editor swaps the active skin for the whole site.
Users who do not install a skin extension do not pay its runtime cost.

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.
Stable anatomy wrappers such as control-content are allowed when they create a reusable layering contract for skins and extensions.
Anatomy wrappers should be minimal, named with 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.tsx
components/refined-ui/hooks/use-chat-message.ts
components/refined-ui/blocks/chat.tsx
components/ui/button.tsx

On this page

  • The layers
  • Runtime-agnostic UI
  • One library, skins as data
  • The customization ladder
  • Extensions, anchors, and integrated behaviors
  • Versions of a name vs versions of a usage
  • The skin config file
  • Composition choices
  • Registry source of truth