Radix UI Design System
Critical rules (non-obvious)
- Always use
asChildon Trigger/Close — prevents nested<button>inside<button>DOM errors - Always include
<Dialog.Title>and<Dialog.Description>— screen readers require them; omitting causes a11y violations - Never mix controlled + uncontrolled — don't use both
defaultValueandvalueon same component - Animations need
forceMount— without it Portal unmounts instantly, killing Framer Motion exit animations - Dropdown positioning off? — parent has
overflow: hiddenor CSS transform; always wrap in<Portal>
Core pattern: Dialog
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild><button>Open</button></Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="overlay" />
<Dialog.Content className="content">
<Dialog.Title>Title</Dialog.Title> {/* required */}
<Dialog.Description>Desc</Dialog.Description> {/* required */}
<Dialog.Close asChild><button>Close</button></Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
Theming: CSS variables (recommended)
:root { --color-primary: 220 90% 56%; --radius: 0.5rem; }
[data-theme="dark"] { --color-primary: 220 90% 66%; }
<Dialog.Content className="bg-[hsl(var(--color-primary))] rounded-[var(--radius)]" />
Theming: CVA for variants
import { cva } from 'class-variance-authority';
const button = cva("base-styles", {
variants: {
variant: { default: "bg-primary", destructive: "bg-red-500" },
size: { sm: "h-9 px-3", md: "h-10 px-4" },
},
defaultVariants: { variant: "default", size: "md" },
});
Animation with Framer Motion
<Dialog.Portal forceMount> {/* forceMount: critical */}
<AnimatePresence>
{open && (
<Dialog.Overlay asChild>
<motion.div initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}} />
</Dialog.Overlay>
)}
</AnimatePresence>
</Dialog.Portal>
Hook form integration (Select)
<Controller name="country" control={control} render={({ field }) => (
<Select.Root onValueChange={field.onChange} value={field.value}>
<Select.Trigger><Select.Value placeholder="Select..." /><Select.Icon /></Select.Trigger>
<Select.Portal>
<Select.Content><Select.Viewport>
<Select.Item value="us"><Select.ItemText>US</Select.ItemText></Select.Item>
</Select.Viewport></Select.Content>
</Select.Portal>
</Select.Root>
)} />
Primitives quick reference
| Component | Key parts | |---|---| | Dialog | Root · Trigger · Portal · Overlay · Content · Title · Description · Close | | DropdownMenu | Root · Trigger · Portal · Content · Item · Separator · CheckboxItem · RadioGroup · Sub | | Tabs | Root · List · Trigger · Content | | Tooltip | Provider · Root · Trigger · Portal · Content · Arrow | | Select | Root · Trigger · Value · Icon · Portal · Content · Viewport · Item · ItemText · ItemIndicator |
shadcn vs raw Radix
- shadcn: quick prototyping, standard designs; run
npx shadcn@latest add dialog - Raw Radix: full customization, unique designs, zero-dependency component library