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. shadcn compatibility
Guide

shadcn compatibility

One library, compatible by contract: shadcn's token names, shadcn's idioms, your components/ui untouched.

Compatibility is a contract, not a copy

Refined UI does not ship a parallel "shadcn version" of itself. ONE library ships, and it stays compatible with shadcn the way a good citizen does: it speaks shadcn's token language, keeps shadcn's component idioms, installs through the same npx shadcn add workflow, and lives beside your existing components/ui/* without ever writing to it.

Same install workflow: every item is a shadcn registry manifest - npx shadcn@latest add <registry-url>/r/refined/<item>.json - and the installed files are yours to edit.
Same token language: the core color contract carries shadcn's exact names, so a shadcn theme drops in unchanged.
Same component idioms: cn() merging, caller className always winning, data-slot anatomy, variant props - shadcn muscle memory transfers.
Separate trees: Refined UI installs under components/refined-ui/*; your components/ui/* stays untouched and both coexist in one app.

Bring your shadcn theme

The token contract's core rung IS shadcn's theme vocabulary: --background, --foreground, --card, --popover, --primary, --secondary, --muted, --accent, --destructive, --border, --input, --ring (plus their -foreground pairs). Paste the :root / .dark block you already have and the library wears your brand directly.

/* your existing shadcn theme keeps working as-is */
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.6131 0.2067 283.83);
--primary-foreground: oklch(1 0 0);
--radius: 0.625rem;
/* … the rest of your shadcn variables … */
}
​
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
/* … */
}
This works by construction: the baseline authors every default at zero specificity (:where(:root)), so any value your app writes wins - no !important, no ordering games.
Beyond the core rung the contract adds what agent surfaces need - a type scale (--text-body, --text-label), a parametric shadow recipe, motion durations, per-surface radii. All have baseline defaults; adopt only what you brand.
Every token is a literal value (oklch/px/rem/ms), so the theme editor can read and re-write your theme knob by knob.

Your app's theme: :root, not a pack

The skin packs in this registry scope their tokens to [data-skin="<id>"]. That is the DISTRIBUTION format - it lets ten packs compile in one Tailwind build (this docs site does exactly that) and lets portalled popups re-assert their scope. Your app does not need it: one brand, worn forever, is a plain :root / .dark block - the shadcn convention above.

Tweak a few tokens: write them in your own globals.css under :root / .dark. Done - no pack, no attribute, no name.
A full design system that stays yours: still :root / .dark, plus skin.config slot overrides when you restyle what recipes paint - the config is a static module the components already read.
A skin you want to distribute or switch: author the three-file pack (theme.css scoped to your id, skin.config, skin.css) and stamp data-skin="<id>" once on <html>. The same artifact then works in any app, the theme editor can hydrate from it, and several skins can coexist.
The pack id is your design system's NAME (like every VS Code theme has one), not switching machinery - the scoping serves portals, leak-prevention, and merge-free library updates.

shadcn-shaped APIs on Base UI

Components are Base UI under the hood with a deliberate shadcn facade: TypeScript namespaces flattened to named exports, data-state attributes where shadcn consumers expect them, ComponentProps-typed parts, and compound anatomy instead of prop bags. If you can compose a shadcn Dialog, you can compose these.

Caller-wins is the contract: every slot renders cn(recipe, skin override, your className) - your class is always last and always final.
Anatomy is addressable: data-ui="agent", data-slot, data-control, data-variant, data-tone, data-size - stable hooks for CSS, tests, and extensions.
Registry-first ownership, exactly like shadcn: the source lands in your repo and editing it is the sanctioned last rung of the customization ladder.

Migrating a shadcn app

There is no big-bang migration: both trees coexist, so adopt surface by surface.

Keep components/ui/* for everything that already works; nothing in Refined UI depends on it or overwrites it.
Install the agent surfaces you need (chat, tool-call, blocks) - they bring their own slots under components/refined-ui/ui/*.
Want a Refined UI primitive where a shadcn one sits today? Swap the import from @/components/ui/x to @/components/refined-ui/ui/x at the call sites you choose - the props read the same.
Your theme carries over first (this page's :root story), so the new surfaces match your brand from the first render.

On this page

  • Compatibility is a contract, not a copy
  • Bring your shadcn theme
  • Your app's theme: :root, not a pack
  • shadcn-shaped APIs on Base UI
  • Migrating a shadcn app