Headless UI Components: Why shadcn/ui and Radix UI Make a Difference

Headless UI Components: Why shadcn/ui and Radix UI Make a Difference

Component Architecture8 min readJanuary 20, 2026

A deep dive into headless UI libraries, comparing Radix UI and shadcn/ui for building accessible, scalable React components.

Headless UI delivers behavior without opinions about style. Rather than pre-styled components, these libraries provide accessibility-correct primitives with focus traps, ARIA roles, and keyboard interactions, letting teams control visuals through CSS, Tailwind, or design tokens.

Headless vs Styled Component Libraries

Styled kits (MUI, Ant Design, Chakra) ship pre-built visuals - fast to start but expensive to retheme deeply.

Headless (Radix UI, React Aria, Headless UI, Ark UI) provide only behaviors. Teams supply styles and tokens, which keeps branding consistent, eliminates wrestling with opinionated CSS, and prevents vendor lock-in.

Radix UI vs shadcn/ui

Radix UI: Low-level, unstyled primitives (Dialog, Popover, Dropdown Menu, Tabs, Tooltip). Teams compose and style them with their system.

shadcn/ui: A generator that copies ownable components into your codebase. Under the hood, components use Radix primitives plus Tailwind with clean variants and tokens.

Choose Radix for maximum control. Choose shadcn/ui for a solid foundation you can own and modify.

Composing Accessible Primitives

Dialog

"use client";
import * as Dialog from "@radix-ui/react-dialog";
 
export function Modal({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <Dialog.Root>
      <Dialog.Trigger asChild>
        <button className="rounded-md bg-brand px-4 py-2 text-white">Open</button>
      </Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 bg-black/50" />
        <Dialog.Content className="fixed left-1/2 top-1/2 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg bg-bg p-6 shadow-xl">
          <Dialog.Title className="text-lg font-semibold">{title}</Dialog.Title>
          <div className="mt-4">{children}</div>
          <Dialog.Close asChild>
            <button className="mt-6 rounded-md border px-4 py-2">Close</button>
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}
"use client";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
 
export function UserMenu() {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger className="rounded-md border px-3 py-2">Menu</DropdownMenu.Trigger>
      <DropdownMenu.Content className="z-50 min-w-[180px] rounded-md border bg-bg p-1 shadow-md">
        <DropdownMenu.Item className="rounded px-2 py-1.5 focus:bg-fg/10">Profile</DropdownMenu.Item>
        <DropdownMenu.Item className="rounded px-2 py-1.5 focus:bg-fg/10">Settings</DropdownMenu.Item>
        <DropdownMenu.Separator className="my-1 h-px bg-fg/10" />
        <DropdownMenu.Item className="rounded px-2 py-1.5 text-red-600">Sign out</DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu.Root>
  );
}

Tokens and Theming

Headless UI excels when paired with tokens. Define CSS variables for color, spacing, radius, and motion, then map Tailwind to those variables:

export default {
  darkMode: ["class", '[data-theme="dark"]'],
  theme: {
    extend: {
      colors: {
        bg: { DEFAULT: "var(--color-bg-default)" },
        fg: { DEFAULT: "var(--color-fg-default)", muted: "var(--color-fg-muted)" },
        brand: { DEFAULT: "var(--color-brand-primary)" },
      },
    },
  },
};

SSR and App Router Considerations

Keep interactive components as Client Components. Render data on the server. For route-bound modals, use parallel routes and intercepting routes so dialogs have URLs. Leverage loading.tsx for skeletons and Suspense for deferred sections.

Decision Matrix

LibraryStylingBest for
Radix UIYour CSS/TailwindCustom design systems, full control
shadcn/uiTailwind classesFast bootstrap with source ownership
Headless UIYour CSS/TailwindTailwind-first projects
React AriaYour stylingFine-grained control, ARIA hooks

The choice depends on how much control you need vs how fast you want to ship.