We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide37 min read

Radix UI

Radix UI is unstyled, fully accessible React components. The foundation of shadcn/ui and modern UI libraries.

Radix UI - Fundament Dostępnych Komponentów React

Czym jest Radix UI?

Radix UI to biblioteka nieostylowanych, w pełni dostępnych komponentów React (primitives). Jest fundamentem popularnej biblioteki shadcn/ui i wielu innych nowoczesnych systemów UI. Radix dostarcza wszystkie skomplikowane zachowania (keyboard navigation, focus management, ARIA attributes) bez narzucania stylów - masz pełną kontrolę nad wyglądem.

Dlaczego Radix UI?

Problemy z tradycyjnymi bibliotekami UI

Tradycyjne biblioteki komponentów (Material UI, Ant Design, Bootstrap) mają wspólne problemy:

  1. Narzucone style - Trudno dostosować do własnego designu
  2. Duży bundle size - Nawet jeśli używasz jednego komponentu
  3. Niepełna dostępność - Często pomijane edge cases
  4. Ograniczona kompozycja - Sztywna struktura komponentów

Zalety Radix UI

  1. Pełna dostępność (a11y) - WAI-ARIA compliant, testowane z screen readerami
  2. Nieostylowane - 100% kontroli nad CSS, zero konfliktów
  3. Kompozycyjne API - Elastyczne budowanie UI z prymitywów
  4. Małe rozmiary - Importuj tylko to, czego potrzebujesz
  5. Animacje - Pełne wsparcie dla Framer Motion i CSS animations
  6. SSR - Działa z Next.js out of the box

Instalacja

Pojedyncze komponenty

Code
Bash
# Każdy prymityw instalujemy osobno
npm install @radix-ui/react-dialog
npm install @radix-ui/react-dropdown-menu
npm install @radix-ui/react-popover
npm install @radix-ui/react-tooltip
npm install @radix-ui/react-tabs
npm install @radix-ui/react-accordion
npm install @radix-ui/react-switch
npm install @radix-ui/react-checkbox
npm install @radix-ui/react-select
npm install @radix-ui/react-slider

Ikony (opcjonalnie)

Code
Bash
npm install @radix-ui/react-icons

Dialog (Modal)

Dialog to jeden z najczęściej używanych komponentów. Radix obsługuje wszystkie edge cases:

TScomponents/Modal.tsx
TypeScript
// components/Modal.tsx
import * as Dialog from '@radix-ui/react-dialog'
import { X } from 'lucide-react'

interface ModalProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  title: string
  description?: string
  children: React.ReactNode
}

export function Modal({
  open,
  onOpenChange,
  title,
  description,
  children,
}: ModalProps) {
  return (
    <Dialog.Root open={open} onOpenChange={onOpenChange}>
      <Dialog.Portal>
        {/* Overlay - tło za modalem */}
        <Dialog.Overlay
          className="fixed inset-0 bg-black/50 backdrop-blur-sm
            data-[state=open]:animate-in data-[state=closed]:animate-out
            data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
        />

        {/* Content - sam modal */}
        <Dialog.Content
          className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2
            w-full max-w-lg bg-white dark:bg-gray-900
            rounded-xl shadow-xl p-6
            data-[state=open]:animate-in data-[state=closed]:animate-out
            data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
            data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
            data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]
            data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]"
        >
          {/* Nagłówek */}
          <Dialog.Title className="text-xl font-semibold mb-2">
            {title}
          </Dialog.Title>

          {description && (
            <Dialog.Description className="text-gray-500 mb-4">
              {description}
            </Dialog.Description>
          )}

          {/* Zawartość */}
          <div className="mt-4">
            {children}
          </div>

          {/* Przycisk zamknięcia */}
          <Dialog.Close asChild>
            <button
              className="absolute top-4 right-4 p-1 rounded-full
                hover:bg-gray-100 dark:hover:bg-gray-800
                focus:outline-none focus:ring-2 focus:ring-blue-500"
              aria-label="Zamknij"
            >
              <X className="w-5 h-5" />
            </button>
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  )
}

// Przykład użycia
function Example() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <button onClick={() => setOpen(true)}>
        Otwórz modal
      </button>

      <Modal
        open={open}
        onOpenChange={setOpen}
        title="Potwierdź akcję"
        description="Czy na pewno chcesz kontynuować?"
      >
        <div className="flex gap-3 justify-end">
          <Dialog.Close asChild>
            <button className="px-4 py-2 rounded bg-gray-100">
              Anuluj
            </button>
          </Dialog.Close>
          <button
            className="px-4 py-2 rounded bg-blue-500 text-white"
            onClick={() => {
              // Akcja
              setOpen(false)
            }}
          >
            Potwierdź
          </button>
        </div>
      </Modal>
    </>
  )
}

Dialog z triggerem

Code
TypeScript
<Dialog.Root>
  <Dialog.Trigger asChild>
    <button className="px-4 py-2 bg-blue-500 text-white rounded">
      Otwórz
    </button>
  </Dialog.Trigger>

  <Dialog.Portal>
    <Dialog.Overlay className="fixed inset-0 bg-black/50" />
    <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded-lg">
      <Dialog.Title>Tytuł</Dialog.Title>
      <Dialog.Description>Opis...</Dialog.Description>
      <Dialog.Close asChild>
        <button>Zamknij</button>
      </Dialog.Close>
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

Dropdown Menu

Kompleksowe menu rozwijane z pełną obsługą klawiatury:

TScomponents/DropdownMenu.tsx
TypeScript
// components/DropdownMenu.tsx
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import {
  User,
  Settings,
  CreditCard,
  LogOut,
  ChevronRight,
  Check
} from 'lucide-react'

interface UserMenuProps {
  user: {
    name: string
    email: string
    avatar: string
  }
  onLogout: () => void
}

export function UserMenu({ user, onLogout }: UserMenuProps) {
  const [theme, setTheme] = useState('system')
  const [notifications, setNotifications] = useState(true)

  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <button className="flex items-center gap-2 p-2 rounded-full hover:bg-gray-100">
          <img
            src={user.avatar}
            alt={user.name}
            className="w-8 h-8 rounded-full"
          />
        </button>
      </DropdownMenu.Trigger>

      <DropdownMenu.Portal>
        <DropdownMenu.Content
          className="min-w-[220px] bg-white dark:bg-gray-900
            rounded-lg shadow-lg border border-gray-200 dark:border-gray-800
            p-1.5 animate-in fade-in-0 zoom-in-95"
          sideOffset={8}
          align="end"
        >
          {/* Nagłówek z informacjami o użytkowniku */}
          <DropdownMenu.Label className="px-2 py-1.5">
            <p className="font-medium">{user.name}</p>
            <p className="text-sm text-gray-500">{user.email}</p>
          </DropdownMenu.Label>

          <DropdownMenu.Separator className="h-px bg-gray-200 dark:bg-gray-800 my-1" />

          {/* Zwykłe elementy menu */}
          <DropdownMenu.Item
            className="flex items-center gap-2 px-2 py-1.5 rounded
              cursor-pointer outline-none
              hover:bg-gray-100 dark:hover:bg-gray-800
              focus:bg-gray-100 dark:focus:bg-gray-800"
          >
            <User className="w-4 h-4" />
            <span>Profil</span>
          </DropdownMenu.Item>

          <DropdownMenu.Item className="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer outline-none hover:bg-gray-100 focus:bg-gray-100">
            <CreditCard className="w-4 h-4" />
            <span>Płatności</span>
          </DropdownMenu.Item>

          <DropdownMenu.Item className="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer outline-none hover:bg-gray-100 focus:bg-gray-100">
            <Settings className="w-4 h-4" />
            <span>Ustawienia</span>
          </DropdownMenu.Item>

          <DropdownMenu.Separator className="h-px bg-gray-200 my-1" />

          {/* Podmenu */}
          <DropdownMenu.Sub>
            <DropdownMenu.SubTrigger
              className="flex items-center justify-between px-2 py-1.5 rounded
                cursor-pointer outline-none
                hover:bg-gray-100 focus:bg-gray-100
                data-[state=open]:bg-gray-100"
            >
              <span>Motyw</span>
              <ChevronRight className="w-4 h-4" />
            </DropdownMenu.SubTrigger>

            <DropdownMenu.Portal>
              <DropdownMenu.SubContent
                className="min-w-[160px] bg-white rounded-lg shadow-lg border p-1.5"
                sideOffset={4}
              >
                <DropdownMenu.RadioGroup value={theme} onValueChange={setTheme}>
                  <DropdownMenu.RadioItem
                    value="light"
                    className="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer outline-none hover:bg-gray-100"
                  >
                    <DropdownMenu.ItemIndicator>
                      <Check className="w-4 h-4" />
                    </DropdownMenu.ItemIndicator>
                    <span className="pl-6">Jasny</span>
                  </DropdownMenu.RadioItem>

                  <DropdownMenu.RadioItem
                    value="dark"
                    className="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer outline-none hover:bg-gray-100"
                  >
                    <DropdownMenu.ItemIndicator>
                      <Check className="w-4 h-4" />
                    </DropdownMenu.ItemIndicator>
                    <span className="pl-6">Ciemny</span>
                  </DropdownMenu.RadioItem>

                  <DropdownMenu.RadioItem
                    value="system"
                    className="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer outline-none hover:bg-gray-100"
                  >
                    <DropdownMenu.ItemIndicator>
                      <Check className="w-4 h-4" />
                    </DropdownMenu.ItemIndicator>
                    <span className="pl-6">System</span>
                  </DropdownMenu.RadioItem>
                </DropdownMenu.RadioGroup>
              </DropdownMenu.SubContent>
            </DropdownMenu.Portal>
          </DropdownMenu.Sub>

          {/* Checkbox item */}
          <DropdownMenu.CheckboxItem
            checked={notifications}
            onCheckedChange={setNotifications}
            className="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer outline-none hover:bg-gray-100"
          >
            <DropdownMenu.ItemIndicator>
              <Check className="w-4 h-4" />
            </DropdownMenu.ItemIndicator>
            <span className="pl-6">Powiadomienia</span>
          </DropdownMenu.CheckboxItem>

          <DropdownMenu.Separator className="h-px bg-gray-200 my-1" />

          {/* Logout z custom styling */}
          <DropdownMenu.Item
            onClick={onLogout}
            className="flex items-center gap-2 px-2 py-1.5 rounded
              cursor-pointer outline-none
              text-red-600 hover:bg-red-50 focus:bg-red-50"
          >
            <LogOut className="w-4 h-4" />
            <span>Wyloguj się</span>
          </DropdownMenu.Item>

          {/* Strzałka wskazująca na trigger */}
          <DropdownMenu.Arrow className="fill-white" />
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}

Popover

Idealne do tooltipów, edycji inline i floating panels:

TScomponents/Popover.tsx
TypeScript
// components/Popover.tsx
import * as Popover from '@radix-ui/react-popover'
import { Settings, X } from 'lucide-react'

export function SettingsPopover() {
  return (
    <Popover.Root>
      <Popover.Trigger asChild>
        <button
          className="p-2 rounded-lg hover:bg-gray-100"
          aria-label="Ustawienia"
        >
          <Settings className="w-5 h-5" />
        </button>
      </Popover.Trigger>

      <Popover.Portal>
        <Popover.Content
          className="w-80 bg-white dark:bg-gray-900 rounded-lg shadow-xl
            border border-gray-200 dark:border-gray-800 p-4
            animate-in fade-in-0 zoom-in-95
            data-[side=bottom]:slide-in-from-top-2
            data-[side=left]:slide-in-from-right-2
            data-[side=right]:slide-in-from-left-2
            data-[side=top]:slide-in-from-bottom-2"
          sideOffset={8}
        >
          <div className="flex justify-between items-center mb-4">
            <h3 className="font-semibold">Ustawienia wyświetlania</h3>
            <Popover.Close asChild>
              <button className="p-1 rounded hover:bg-gray-100">
                <X className="w-4 h-4" />
              </button>
            </Popover.Close>
          </div>

          <div className="space-y-4">
            <div>
              <label className="block text-sm font-medium mb-1">
                Rozmiar tekstu
              </label>
              <select className="w-full px-3 py-2 border rounded-lg">
                <option>Mały</option>
                <option>Średni</option>
                <option>Duży</option>
              </select>
            </div>

            <div>
              <label className="block text-sm font-medium mb-1">
                Kontrast
              </label>
              <input
                type="range"
                min="0"
                max="100"
                className="w-full"
              />
            </div>

            <div className="flex items-center justify-between">
              <span className="text-sm">Tryb ciemny</span>
              <Switch />
            </div>
          </div>

          <Popover.Arrow className="fill-white" />
        </Popover.Content>
      </Popover.Portal>
    </Popover.Root>
  )
}

Tooltip

Lekkie tooltips z pełną obsługą dostępności:

TScomponents/Tooltip.tsx
TypeScript
// components/Tooltip.tsx
import * as Tooltip from '@radix-ui/react-tooltip'

interface TooltipWrapperProps {
  content: string
  children: React.ReactNode
  side?: 'top' | 'right' | 'bottom' | 'left'
  delayDuration?: number
}

export function TooltipWrapper({
  content,
  children,
  side = 'top',
  delayDuration = 200,
}: TooltipWrapperProps) {
  return (
    <Tooltip.Provider delayDuration={delayDuration}>
      <Tooltip.Root>
        <Tooltip.Trigger asChild>
          {children}
        </Tooltip.Trigger>

        <Tooltip.Portal>
          <Tooltip.Content
            side={side}
            className="px-3 py-1.5 bg-gray-900 text-white text-sm rounded-lg
              animate-in fade-in-0 zoom-in-95
              data-[side=bottom]:slide-in-from-top-2
              data-[side=left]:slide-in-from-right-2
              data-[side=right]:slide-in-from-left-2
              data-[side=top]:slide-in-from-bottom-2"
            sideOffset={4}
          >
            {content}
            <Tooltip.Arrow className="fill-gray-900" />
          </Tooltip.Content>
        </Tooltip.Portal>
      </Tooltip.Root>
    </Tooltip.Provider>
  )
}

// Użycie
function Example() {
  return (
    <TooltipWrapper content="Kliknij, aby skopiować">
      <button className="p-2 rounded hover:bg-gray-100">
        <Copy className="w-5 h-5" />
      </button>
    </TooltipWrapper>
  )
}

Provider na poziomie aplikacji

TSapp/providers.tsx
TypeScript
// app/providers.tsx
import * as Tooltip from '@radix-ui/react-tooltip'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <Tooltip.Provider delayDuration={200} skipDelayDuration={500}>
      {children}
    </Tooltip.Provider>
  )
}

Tabs

Zakładki z pełną obsługą klawiatury (strzałki, Home, End):

TScomponents/Tabs.tsx
TypeScript
// components/Tabs.tsx
import * as Tabs from '@radix-ui/react-tabs'

interface Tab {
  value: string
  label: string
  content: React.ReactNode
  disabled?: boolean
}

interface TabsProps {
  tabs: Tab[]
  defaultValue?: string
}

export function TabsComponent({ tabs, defaultValue }: TabsProps) {
  return (
    <Tabs.Root
      defaultValue={defaultValue || tabs[0]?.value}
      className="w-full"
    >
      <Tabs.List
        className="flex border-b border-gray-200"
        aria-label="Zarządzaj kontem"
      >
        {tabs.map((tab) => (
          <Tabs.Trigger
            key={tab.value}
            value={tab.value}
            disabled={tab.disabled}
            className="px-4 py-2 text-sm font-medium
              border-b-2 border-transparent
              text-gray-500 hover:text-gray-700
              data-[state=active]:border-blue-500
              data-[state=active]:text-blue-600
              disabled:opacity-50 disabled:cursor-not-allowed
              focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
          >
            {tab.label}
          </Tabs.Trigger>
        ))}
      </Tabs.List>

      {tabs.map((tab) => (
        <Tabs.Content
          key={tab.value}
          value={tab.value}
          className="pt-4 focus:outline-none"
        >
          {tab.content}
        </Tabs.Content>
      ))}
    </Tabs.Root>
  )
}

// Użycie
function AccountSettings() {
  const tabs = [
    {
      value: 'profile',
      label: 'Profil',
      content: <ProfileForm />,
    },
    {
      value: 'security',
      label: 'Bezpieczeństwo',
      content: <SecuritySettings />,
    },
    {
      value: 'notifications',
      label: 'Powiadomienia',
      content: <NotificationSettings />,
    },
    {
      value: 'billing',
      label: 'Płatności',
      content: <BillingInfo />,
      disabled: true, // Niedostępne
    },
  ]

  return <TabsComponent tabs={tabs} defaultValue="profile" />
}

Vertical Tabs

Code
TypeScript
<Tabs.Root
  defaultValue="tab1"
  orientation="vertical"
  className="flex gap-4"
>
  <Tabs.List className="flex flex-col w-48 border-r">
    <Tabs.Trigger value="tab1" className="px-4 py-2 text-left data-[state=active]:bg-gray-100">
      Tab 1
    </Tabs.Trigger>
    <Tabs.Trigger value="tab2" className="px-4 py-2 text-left data-[state=active]:bg-gray-100">
      Tab 2
    </Tabs.Trigger>
  </Tabs.List>

  <div className="flex-1">
    <Tabs.Content value="tab1">Content 1</Tabs.Content>
    <Tabs.Content value="tab2">Content 2</Tabs.Content>
  </div>
</Tabs.Root>

Accordion

Rozwijane sekcje z animacjami:

TScomponents/Accordion.tsx
TypeScript
// components/Accordion.tsx
import * as Accordion from '@radix-ui/react-accordion'
import { ChevronDown } from 'lucide-react'

interface AccordionItem {
  value: string
  trigger: string
  content: React.ReactNode
}

interface AccordionProps {
  items: AccordionItem[]
  type?: 'single' | 'multiple'
  defaultValue?: string | string[]
}

export function AccordionComponent({
  items,
  type = 'single',
  defaultValue,
}: AccordionProps) {
  return (
    <Accordion.Root
      type={type}
      defaultValue={defaultValue}
      collapsible={type === 'single'}
      className="w-full"
    >
      {items.map((item) => (
        <Accordion.Item
          key={item.value}
          value={item.value}
          className="border-b border-gray-200"
        >
          <Accordion.Header>
            <Accordion.Trigger
              className="flex w-full items-center justify-between
                py-4 font-medium text-left
                hover:underline
                [&[data-state=open]>svg]:rotate-180"
            >
              {item.trigger}
              <ChevronDown
                className="w-5 h-5 text-gray-500 transition-transform duration-200"
              />
            </Accordion.Trigger>
          </Accordion.Header>

          <Accordion.Content
            className="overflow-hidden
              data-[state=closed]:animate-accordion-up
              data-[state=open]:animate-accordion-down"
          >
            <div className="pb-4 text-gray-600">
              {item.content}
            </div>
          </Accordion.Content>
        </Accordion.Item>
      ))}
    </Accordion.Root>
  )
}

// Tailwind config dla animacji
// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      keyframes: {
        'accordion-down': {
          from: { height: '0' },
          to: { height: 'var(--radix-accordion-content-height)' },
        },
        'accordion-up': {
          from: { height: 'var(--radix-accordion-content-height)' },
          to: { height: '0' },
        },
      },
      animation: {
        'accordion-down': 'accordion-down 0.2s ease-out',
        'accordion-up': 'accordion-up 0.2s ease-out',
      },
    },
  },
}

Select

Pełny select z wyszukiwaniem, grupami i keyboard navigation:

TScomponents/Select.tsx
TypeScript
// components/Select.tsx
import * as Select from '@radix-ui/react-select'
import { Check, ChevronDown, ChevronUp } from 'lucide-react'

interface SelectOption {
  value: string
  label: string
  disabled?: boolean
}

interface SelectGroup {
  label: string
  options: SelectOption[]
}

interface SelectProps {
  placeholder?: string
  value?: string
  onValueChange: (value: string) => void
  groups: SelectGroup[]
}

export function SelectComponent({
  placeholder = 'Wybierz...',
  value,
  onValueChange,
  groups,
}: SelectProps) {
  return (
    <Select.Root value={value} onValueChange={onValueChange}>
      <Select.Trigger
        className="flex items-center justify-between w-full px-3 py-2
          border border-gray-300 rounded-lg bg-white
          focus:outline-none focus:ring-2 focus:ring-blue-500
          data-[placeholder]:text-gray-400"
      >
        <Select.Value placeholder={placeholder} />
        <Select.Icon>
          <ChevronDown className="w-4 h-4 text-gray-500" />
        </Select.Icon>
      </Select.Trigger>

      <Select.Portal>
        <Select.Content
          className="overflow-hidden bg-white rounded-lg shadow-lg border
            animate-in fade-in-0 zoom-in-95"
          position="popper"
          sideOffset={4}
        >
          <Select.ScrollUpButton className="flex items-center justify-center h-6 bg-white">
            <ChevronUp className="w-4 h-4" />
          </Select.ScrollUpButton>

          <Select.Viewport className="p-1">
            {groups.map((group, groupIndex) => (
              <Select.Group key={group.label}>
                {groupIndex > 0 && (
                  <Select.Separator className="h-px bg-gray-200 my-1" />
                )}

                <Select.Label className="px-2 py-1.5 text-xs font-medium text-gray-500">
                  {group.label}
                </Select.Label>

                {group.options.map((option) => (
                  <Select.Item
                    key={option.value}
                    value={option.value}
                    disabled={option.disabled}
                    className="relative flex items-center px-8 py-2 rounded
                      cursor-pointer outline-none
                      hover:bg-gray-100 focus:bg-gray-100
                      data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed"
                  >
                    <Select.ItemIndicator className="absolute left-2">
                      <Check className="w-4 h-4" />
                    </Select.ItemIndicator>
                    <Select.ItemText>{option.label}</Select.ItemText>
                  </Select.Item>
                ))}
              </Select.Group>
            ))}
          </Select.Viewport>

          <Select.ScrollDownButton className="flex items-center justify-center h-6 bg-white">
            <ChevronDown className="w-4 h-4" />
          </Select.ScrollDownButton>
        </Select.Content>
      </Select.Portal>
    </Select.Root>
  )
}

// Użycie
function CountrySelect() {
  const [country, setCountry] = useState('')

  const groups = [
    {
      label: 'Europa',
      options: [
        { value: 'pl', label: 'Polska' },
        { value: 'de', label: 'Niemcy' },
        { value: 'fr', label: 'Francja' },
      ],
    },
    {
      label: 'Ameryka',
      options: [
        { value: 'us', label: 'Stany Zjednoczone' },
        { value: 'ca', label: 'Kanada' },
      ],
    },
  ]

  return (
    <SelectComponent
      placeholder="Wybierz kraj"
      value={country}
      onValueChange={setCountry}
      groups={groups}
    />
  )
}

Switch

Toggle switch z animacją:

TScomponents/Switch.tsx
TypeScript
// components/Switch.tsx
import * as Switch from '@radix-ui/react-switch'

interface SwitchProps {
  checked: boolean
  onCheckedChange: (checked: boolean) => void
  label?: string
  disabled?: boolean
}

export function SwitchComponent({
  checked,
  onCheckedChange,
  label,
  disabled = false,
}: SwitchProps) {
  return (
    <div className="flex items-center gap-3">
      <Switch.Root
        checked={checked}
        onCheckedChange={onCheckedChange}
        disabled={disabled}
        className="w-11 h-6 bg-gray-200 rounded-full
          relative cursor-pointer
          data-[state=checked]:bg-blue-500
          disabled:opacity-50 disabled:cursor-not-allowed
          focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
      >
        <Switch.Thumb
          className="block w-5 h-5 bg-white rounded-full shadow
            transition-transform duration-200
            translate-x-0.5 data-[state=checked]:translate-x-[22px]"
        />
      </Switch.Root>

      {label && (
        <label className="text-sm font-medium">
          {label}
        </label>
      )}
    </div>
  )
}

// Użycie
function NotificationSettings() {
  const [emailNotifications, setEmailNotifications] = useState(true)
  const [pushNotifications, setPushNotifications] = useState(false)

  return (
    <div className="space-y-4">
      <SwitchComponent
        checked={emailNotifications}
        onCheckedChange={setEmailNotifications}
        label="Powiadomienia email"
      />

      <SwitchComponent
        checked={pushNotifications}
        onCheckedChange={setPushNotifications}
        label="Powiadomienia push"
      />
    </div>
  )
}

Checkbox

Checkbox z indeterminate state:

TScomponents/Checkbox.tsx
TypeScript
// components/Checkbox.tsx
import * as Checkbox from '@radix-ui/react-checkbox'
import { Check, Minus } from 'lucide-react'

interface CheckboxProps {
  checked: boolean | 'indeterminate'
  onCheckedChange: (checked: boolean | 'indeterminate') => void
  label: string
  description?: string
}

export function CheckboxComponent({
  checked,
  onCheckedChange,
  label,
  description,
}: CheckboxProps) {
  return (
    <div className="flex items-start gap-3">
      <Checkbox.Root
        checked={checked}
        onCheckedChange={onCheckedChange}
        className="w-5 h-5 border-2 border-gray-300 rounded
          flex items-center justify-center
          data-[state=checked]:bg-blue-500 data-[state=checked]:border-blue-500
          data-[state=indeterminate]:bg-blue-500 data-[state=indeterminate]:border-blue-500
          focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
      >
        <Checkbox.Indicator>
          {checked === 'indeterminate' ? (
            <Minus className="w-3 h-3 text-white" />
          ) : (
            <Check className="w-3 h-3 text-white" />
          )}
        </Checkbox.Indicator>
      </Checkbox.Root>

      <div>
        <label className="text-sm font-medium cursor-pointer">
          {label}
        </label>
        {description && (
          <p className="text-sm text-gray-500">{description}</p>
        )}
      </div>
    </div>
  )
}

// Lista z "Select All"
function CheckboxList() {
  const [items, setItems] = useState([
    { id: '1', label: 'Element 1', checked: false },
    { id: '2', label: 'Element 2', checked: true },
    { id: '3', label: 'Element 3', checked: false },
  ])

  const allChecked = items.every(item => item.checked)
  const someChecked = items.some(item => item.checked)
  const selectAllState = allChecked ? true : someChecked ? 'indeterminate' : false

  const handleSelectAll = (checked: boolean | 'indeterminate') => {
    if (checked === 'indeterminate') return
    setItems(items.map(item => ({ ...item, checked })))
  }

  const handleItemChange = (id: string, checked: boolean | 'indeterminate') => {
    if (checked === 'indeterminate') return
    setItems(items.map(item =>
      item.id === id ? { ...item, checked } : item
    ))
  }

  return (
    <div className="space-y-3">
      <CheckboxComponent
        checked={selectAllState}
        onCheckedChange={handleSelectAll}
        label="Zaznacz wszystkie"
      />

      <div className="ml-6 space-y-2">
        {items.map(item => (
          <CheckboxComponent
            key={item.id}
            checked={item.checked}
            onCheckedChange={(checked) => handleItemChange(item.id, checked)}
            label={item.label}
          />
        ))}
      </div>
    </div>
  )
}

Slider

Suwak z wieloma wartościami:

TScomponents/Slider.tsx
TypeScript
// components/Slider.tsx
import * as Slider from '@radix-ui/react-slider'

interface SliderProps {
  value: number[]
  onValueChange: (value: number[]) => void
  min?: number
  max?: number
  step?: number
  label?: string
}

export function SliderComponent({
  value,
  onValueChange,
  min = 0,
  max = 100,
  step = 1,
  label,
}: SliderProps) {
  return (
    <div className="w-full">
      {label && (
        <div className="flex justify-between mb-2">
          <span className="text-sm font-medium">{label}</span>
          <span className="text-sm text-gray-500">{value.join(' - ')}</span>
        </div>
      )}

      <Slider.Root
        value={value}
        onValueChange={onValueChange}
        min={min}
        max={max}
        step={step}
        className="relative flex items-center w-full h-5 select-none"
      >
        <Slider.Track className="relative h-2 w-full grow rounded-full bg-gray-200">
          <Slider.Range className="absolute h-full rounded-full bg-blue-500" />
        </Slider.Track>

        {value.map((_, index) => (
          <Slider.Thumb
            key={index}
            className="block w-5 h-5 bg-white border-2 border-blue-500 rounded-full
              shadow focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
              hover:bg-blue-50"
          />
        ))}
      </Slider.Root>
    </div>
  )
}

// Użycie - pojedynczy suwak
function VolumeControl() {
  const [volume, setVolume] = useState([50])

  return (
    <SliderComponent
      value={volume}
      onValueChange={setVolume}
      label="Głośność"
    />
  )
}

// Użycie - zakres
function PriceRange() {
  const [range, setRange] = useState([20, 80])

  return (
    <SliderComponent
      value={range}
      onValueChange={setRange}
      min={0}
      max={1000}
      step={10}
      label="Zakres cen (PLN)"
    />
  )
}

Navigation Menu

Zaawansowane menu nawigacyjne (jak na dużych stronach):

TScomponents/NavigationMenu.tsx
TypeScript
// components/NavigationMenu.tsx
import * as NavigationMenu from '@radix-ui/react-navigation-menu'
import { ChevronDown } from 'lucide-react'

export function MainNavigation() {
  return (
    <NavigationMenu.Root className="relative z-10">
      <NavigationMenu.List className="flex items-center gap-1 p-1">
        {/* Link bez submenu */}
        <NavigationMenu.Item>
          <NavigationMenu.Link
            href="/"
            className="px-3 py-2 rounded-md text-sm font-medium
              hover:bg-gray-100 focus:outline-none focus:ring-2"
          >
            Strona główna
          </NavigationMenu.Link>
        </NavigationMenu.Item>

        {/* Produkty z mega menu */}
        <NavigationMenu.Item>
          <NavigationMenu.Trigger
            className="flex items-center gap-1 px-3 py-2 rounded-md text-sm font-medium
              hover:bg-gray-100 focus:outline-none focus:ring-2
              data-[state=open]:bg-gray-100"
          >
            Produkty
            <ChevronDown className="w-4 h-4 transition-transform data-[state=open]:rotate-180" />
          </NavigationMenu.Trigger>

          <NavigationMenu.Content
            className="absolute top-full left-0 w-full
              data-[motion=from-start]:animate-enterFromLeft
              data-[motion=from-end]:animate-enterFromRight
              data-[motion=to-start]:animate-exitToLeft
              data-[motion=to-end]:animate-exitToRight"
          >
            <div className="grid grid-cols-3 gap-4 p-6 bg-white shadow-lg rounded-lg border">
              <div>
                <h3 className="font-semibold mb-3">Dla Developerów</h3>
                <ul className="space-y-2">
                  <li><a href="/api" className="text-sm hover:text-blue-500">API</a></li>
                  <li><a href="/sdk" className="text-sm hover:text-blue-500">SDK</a></li>
                  <li><a href="/docs" className="text-sm hover:text-blue-500">Dokumentacja</a></li>
                </ul>
              </div>

              <div>
                <h3 className="font-semibold mb-3">Dla Biznesu</h3>
                <ul className="space-y-2">
                  <li><a href="/enterprise" className="text-sm hover:text-blue-500">Enterprise</a></li>
                  <li><a href="/pricing" className="text-sm hover:text-blue-500">Cennik</a></li>
                  <li><a href="/case-studies" className="text-sm hover:text-blue-500">Case Studies</a></li>
                </ul>
              </div>

              <div className="bg-gray-50 rounded-lg p-4">
                <h3 className="font-semibold mb-2">Nowość!</h3>
                <p className="text-sm text-gray-600 mb-3">
                  Sprawdź nasz najnowszy produkt - AI Assistant.
                </p>
                <a
                  href="/ai-assistant"
                  className="text-sm text-blue-500 font-medium"
                >
                  Dowiedz się więcej →
                </a>
              </div>
            </div>
          </NavigationMenu.Content>
        </NavigationMenu.Item>

        {/* Zasoby z listą */}
        <NavigationMenu.Item>
          <NavigationMenu.Trigger
            className="flex items-center gap-1 px-3 py-2 rounded-md text-sm font-medium
              hover:bg-gray-100"
          >
            Zasoby
            <ChevronDown className="w-4 h-4" />
          </NavigationMenu.Trigger>

          <NavigationMenu.Content className="absolute top-full left-0">
            <ul className="w-48 p-2 bg-white shadow-lg rounded-lg border">
              <li>
                <NavigationMenu.Link
                  href="/blog"
                  className="block px-3 py-2 rounded hover:bg-gray-100 text-sm"
                >
                  Blog
                </NavigationMenu.Link>
              </li>
              <li>
                <NavigationMenu.Link
                  href="/tutorials"
                  className="block px-3 py-2 rounded hover:bg-gray-100 text-sm"
                >
                  Tutoriale
                </NavigationMenu.Link>
              </li>
              <li>
                <NavigationMenu.Link
                  href="/changelog"
                  className="block px-3 py-2 rounded hover:bg-gray-100 text-sm"
                >
                  Changelog
                </NavigationMenu.Link>
              </li>
            </ul>
          </NavigationMenu.Content>
        </NavigationMenu.Item>

        <NavigationMenu.Indicator
          className="top-full z-10 flex h-2.5 items-end justify-center overflow-hidden
            data-[state=visible]:animate-in data-[state=visible]:fade-in
            data-[state=hidden]:animate-out data-[state=hidden]:fade-out"
        >
          <div className="relative top-1/2 h-2 w-2 rotate-45 bg-white border" />
        </NavigationMenu.Indicator>
      </NavigationMenu.List>

      <NavigationMenu.Viewport
        className="absolute top-full left-0 flex w-full justify-center"
      />
    </NavigationMenu.Root>
  )
}

Context Menu

Menu kontekstowe (prawy przycisk myszy):

TScomponents/ContextMenu.tsx
TypeScript
// components/ContextMenu.tsx
import * as ContextMenu from '@radix-ui/react-context-menu'
import { Copy, Trash, Edit, Share } from 'lucide-react'

interface FileContextMenuProps {
  children: React.ReactNode
  onCopy: () => void
  onEdit: () => void
  onDelete: () => void
  onShare: () => void
}

export function FileContextMenu({
  children,
  onCopy,
  onEdit,
  onDelete,
  onShare,
}: FileContextMenuProps) {
  return (
    <ContextMenu.Root>
      <ContextMenu.Trigger asChild>
        {children}
      </ContextMenu.Trigger>

      <ContextMenu.Portal>
        <ContextMenu.Content
          className="min-w-[180px] bg-white rounded-lg shadow-lg border p-1
            animate-in fade-in-0 zoom-in-95"
        >
          <ContextMenu.Item
            onClick={onCopy}
            className="flex items-center gap-2 px-2 py-1.5 rounded
              cursor-pointer outline-none hover:bg-gray-100"
          >
            <Copy className="w-4 h-4" />
            Kopiuj
            <ContextMenu.Shortcut className="ml-auto text-xs text-gray-400">
C
            </ContextMenu.Shortcut>
          </ContextMenu.Item>

          <ContextMenu.Item
            onClick={onEdit}
            className="flex items-center gap-2 px-2 py-1.5 rounded
              cursor-pointer outline-none hover:bg-gray-100"
          >
            <Edit className="w-4 h-4" />
            Edytuj
            <ContextMenu.Shortcut className="ml-auto text-xs text-gray-400">
E
            </ContextMenu.Shortcut>
          </ContextMenu.Item>

          <ContextMenu.Item
            onClick={onShare}
            className="flex items-center gap-2 px-2 py-1.5 rounded
              cursor-pointer outline-none hover:bg-gray-100"
          >
            <Share className="w-4 h-4" />
            Udostępnij
          </ContextMenu.Item>

          <ContextMenu.Separator className="h-px bg-gray-200 my-1" />

          <ContextMenu.Item
            onClick={onDelete}
            className="flex items-center gap-2 px-2 py-1.5 rounded
              cursor-pointer outline-none text-red-600 hover:bg-red-50"
          >
            <Trash className="w-4 h-4" />
            Usuń
            <ContextMenu.Shortcut className="ml-auto text-xs">
            </ContextMenu.Shortcut>
          </ContextMenu.Item>
        </ContextMenu.Content>
      </ContextMenu.Portal>
    </ContextMenu.Root>
  )
}

// Użycie
function FileItem({ file }: { file: File }) {
  return (
    <FileContextMenu
      onCopy={() => console.log('Copy', file.name)}
      onEdit={() => console.log('Edit', file.name)}
      onDelete={() => console.log('Delete', file.name)}
      onShare={() => console.log('Share', file.name)}
    >
      <div className="p-4 border rounded hover:bg-gray-50 cursor-pointer">
        <span>{file.name}</span>
      </div>
    </FileContextMenu>
  )
}

Alert Dialog

Dialog potwierdzający destrukcyjne akcje:

TScomponents/AlertDialog.tsx
TypeScript
// components/AlertDialog.tsx
import * as AlertDialog from '@radix-ui/react-alert-dialog'

interface ConfirmDialogProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  title: string
  description: string
  confirmLabel?: string
  cancelLabel?: string
  onConfirm: () => void
  variant?: 'danger' | 'warning'
}

export function ConfirmDialog({
  open,
  onOpenChange,
  title,
  description,
  confirmLabel = 'Potwierdź',
  cancelLabel = 'Anuluj',
  onConfirm,
  variant = 'danger',
}: ConfirmDialogProps) {
  const confirmButtonClass = variant === 'danger'
    ? 'bg-red-500 hover:bg-red-600 text-white'
    : 'bg-yellow-500 hover:bg-yellow-600 text-white'

  return (
    <AlertDialog.Root open={open} onOpenChange={onOpenChange}>
      <AlertDialog.Portal>
        <AlertDialog.Overlay
          className="fixed inset-0 bg-black/50 backdrop-blur-sm
            data-[state=open]:animate-in data-[state=closed]:animate-out
            data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
        />

        <AlertDialog.Content
          className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2
            w-full max-w-md bg-white rounded-xl shadow-xl p-6
            data-[state=open]:animate-in data-[state=closed]:animate-out
            data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
            data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
        >
          <AlertDialog.Title className="text-xl font-semibold">
            {title}
          </AlertDialog.Title>

          <AlertDialog.Description className="mt-2 text-gray-500">
            {description}
          </AlertDialog.Description>

          <div className="flex justify-end gap-3 mt-6">
            <AlertDialog.Cancel asChild>
              <button className="px-4 py-2 rounded-lg border hover:bg-gray-50">
                {cancelLabel}
              </button>
            </AlertDialog.Cancel>

            <AlertDialog.Action asChild>
              <button
                onClick={onConfirm}
                className={`px-4 py-2 rounded-lg ${confirmButtonClass}`}
              >
                {confirmLabel}
              </button>
            </AlertDialog.Action>
          </div>
        </AlertDialog.Content>
      </AlertDialog.Portal>
    </AlertDialog.Root>
  )
}

// Użycie
function DeleteButton() {
  const [open, setOpen] = useState(false)

  const handleDelete = () => {
    // Wykonaj usunięcie
    console.log('Deleted!')
    setOpen(false)
  }

  return (
    <>
      <button
        onClick={() => setOpen(true)}
        className="px-4 py-2 bg-red-500 text-white rounded"
      >
        Usuń konto
      </button>

      <ConfirmDialog
        open={open}
        onOpenChange={setOpen}
        title="Czy na pewno chcesz usunąć konto?"
        description="Ta akcja jest nieodwracalna. Wszystkie Twoje dane zostaną permanentnie usunięte."
        confirmLabel="Usuń konto"
        onConfirm={handleDelete}
        variant="danger"
      />
    </>
  )
}

Animacje z Framer Motion

Radix świetnie współpracuje z Framer Motion:

Code
TypeScript
import * as Dialog from '@radix-ui/react-dialog'
import { motion, AnimatePresence } from 'framer-motion'
import { forwardRef } from 'react'

// Wrapper dla motion z Radix
const MotionOverlay = motion(
  forwardRef<HTMLDivElement, Dialog.DialogOverlayProps>((props, ref) => (
    <Dialog.Overlay ref={ref} {...props} />
  ))
)

const MotionContent = motion(
  forwardRef<HTMLDivElement, Dialog.DialogContentProps>((props, ref) => (
    <Dialog.Content ref={ref} {...props} />
  ))
)

interface AnimatedDialogProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  children: React.ReactNode
}

export function AnimatedDialog({ open, onOpenChange, children }: AnimatedDialogProps) {
  return (
    <Dialog.Root open={open} onOpenChange={onOpenChange}>
      <AnimatePresence>
        {open && (
          <Dialog.Portal forceMount>
            <MotionOverlay
              className="fixed inset-0 bg-black/50"
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
            />

            <MotionContent
              className="fixed left-1/2 top-1/2 w-full max-w-lg bg-white rounded-xl p-6"
              initial={{
                opacity: 0,
                scale: 0.95,
                x: '-50%',
                y: '-50%',
              }}
              animate={{
                opacity: 1,
                scale: 1,
                x: '-50%',
                y: '-50%',
              }}
              exit={{
                opacity: 0,
                scale: 0.95,
                x: '-50%',
                y: '-50%',
              }}
              transition={{
                type: 'spring',
                damping: 25,
                stiffness: 300
              }}
            >
              {children}
            </MotionContent>
          </Dialog.Portal>
        )}
      </AnimatePresence>
    </Dialog.Root>
  )
}

Lista wszystkich prymitywów Radix

KomponentOpisUżycie
DialogModal/popupFormularze, potwierdzenia
Alert DialogDialog potwierdzającyDestrukcyjne akcje
Dropdown MenuMenu rozwijaneAkcje użytkownika
Context MenuMenu kontekstowePrawy przycisk myszy
MenubarPasek menuAplikacje desktop-like
Navigation MenuMenu nawigacyjneGłówna nawigacja
PopoverFloating panelEdycja inline, ustawienia
Hover CardKarta na hoverPreview użytkownika
TooltipPodpowiedźWyjaśnienia UI
AccordionRozwijane sekcjeFAQ, listy
CollapsiblePojedyncza sekcjaWięcej/mniej
TabsZakładkiNawigacja w widoku
TogglePrzycisk toggleOn/off akcje
Toggle GroupGrupa toggleRadio buttons
SelectDropdown selectFormularze
SliderSuwakZakresy wartości
SwitchPrzełącznikBoolean settings
CheckboxCheckboxMulti-select
Radio GroupRadio buttonsSingle select
AvatarAwatarZdjęcie użytkownika
ProgressPasek postępuLoading, progress
Scroll AreaObszar przewijaniaCustom scrollbary
SeparatorSeparatorWizualne oddzielenie
ToolbarPasek narzędziAkcje edytora
ToastPowiadomieniaFeedback użytkownika
Aspect RatioProporcjeMedia containers
FormFormularzWalidacja formularzy
LabelEtykietaAccessibility labels
Visually HiddenUkryty tekstScreen reader only

Podsumowanie

Radix UI to fundament nowoczesnych bibliotek komponentów React. Główne zalety:

  • Pełna dostępność - WAI-ARIA compliant, keyboard navigation
  • Nieostylowane - 100% kontrola nad wyglądem
  • Kompozycyjne API - Elastyczne budowanie UI
  • Małe rozmiary - Importuj tylko potrzebne komponenty
  • SSR ready - Działa z Next.js out of the box
  • Animacje - Pełne wsparcie dla Framer Motion i CSS

Jeśli budujesz własny system UI lub używasz Tailwind CSS, Radix UI jest idealnym wyborem jako fundament komponentów.


Radix UI - The Foundation of Accessible React Components

What is Radix UI?

Radix UI is a library of unstyled, fully accessible React component primitives. It is the foundation of the popular shadcn/ui library and many other modern UI systems. Radix provides all the complex behaviors (keyboard navigation, focus management, ARIA attributes) without imposing any styles - you have complete control over the appearance.

Why Radix UI?

Problems with traditional UI libraries

Traditional component libraries (Material UI, Ant Design, Bootstrap) share common problems:

  1. Imposed styles - Hard to customize to match your own design
  2. Large bundle size - Even if you only use a single component
  3. Incomplete accessibility - Often missing edge cases
  4. Limited composition - Rigid component structure

Advantages of Radix UI

  1. Full accessibility (a11y) - WAI-ARIA compliant, tested with screen readers
  2. Unstyled - 100% control over CSS, zero conflicts
  3. Composable API - Flexible UI building from primitives
  4. Small sizes - Import only what you need
  5. Animations - Full support for Framer Motion and CSS animations
  6. SSR - Works with Next.js out of the box

Installation

Individual components

Code
Bash
# Each primitive is installed separately
npm install @radix-ui/react-dialog
npm install @radix-ui/react-dropdown-menu
npm install @radix-ui/react-popover
npm install @radix-ui/react-tooltip
npm install @radix-ui/react-tabs
npm install @radix-ui/react-accordion
npm install @radix-ui/react-switch
npm install @radix-ui/react-checkbox
npm install @radix-ui/react-select
npm install @radix-ui/react-slider

Icons (optional)

Code
Bash
npm install @radix-ui/react-icons

Dialog (Modal)

Dialog is one of the most commonly used components. Radix handles all edge cases:

TScomponents/Modal.tsx
TypeScript
// components/Modal.tsx
import * as Dialog from '@radix-ui/react-dialog'
import { X } from 'lucide-react'

interface ModalProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  title: string
  description?: string
  children: React.ReactNode
}

export function Modal({
  open,
  onOpenChange,
  title,
  description,
  children,
}: ModalProps) {
  return (
    <Dialog.Root open={open} onOpenChange={onOpenChange}>
      <Dialog.Portal>
        {/* Overlay - background behind the modal */}
        <Dialog.Overlay
          className="fixed inset-0 bg-black/50 backdrop-blur-sm
            data-[state=open]:animate-in data-[state=closed]:animate-out
            data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
        />

        {/* Content - the modal itself */}
        <Dialog.Content
          className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2
            w-full max-w-lg bg-white dark:bg-gray-900
            rounded-xl shadow-xl p-6
            data-[state=open]:animate-in data-[state=closed]:animate-out
            data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
            data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
            data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]
            data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]"
        >
          {/* Header */}
          <Dialog.Title className="text-xl font-semibold mb-2">
            {title}
          </Dialog.Title>

          {description && (
            <Dialog.Description className="text-gray-500 mb-4">
              {description}
            </Dialog.Description>
          )}

          {/* Content */}
          <div className="mt-4">
            {children}
          </div>

          {/* Close button */}
          <Dialog.Close asChild>
            <button
              className="absolute top-4 right-4 p-1 rounded-full
                hover:bg-gray-100 dark:hover:bg-gray-800
                focus:outline-none focus:ring-2 focus:ring-blue-500"
              aria-label="Close"
            >
              <X className="w-5 h-5" />
            </button>
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  )
}

// Usage example
function Example() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <button onClick={() => setOpen(true)}>
        Open modal
      </button>

      <Modal
        open={open}
        onOpenChange={setOpen}
        title="Confirm action"
        description="Are you sure you want to continue?"
      >
        <div className="flex gap-3 justify-end">
          <Dialog.Close asChild>
            <button className="px-4 py-2 rounded bg-gray-100">
              Cancel
            </button>
          </Dialog.Close>
          <button
            className="px-4 py-2 rounded bg-blue-500 text-white"
            onClick={() => {
              // Action
              setOpen(false)
            }}
          >
            Confirm
          </button>
        </div>
      </Modal>
    </>
  )
}

Dialog with trigger

Code
TypeScript
<Dialog.Root>
  <Dialog.Trigger asChild>
    <button className="px-4 py-2 bg-blue-500 text-white rounded">
      Open
    </button>
  </Dialog.Trigger>

  <Dialog.Portal>
    <Dialog.Overlay className="fixed inset-0 bg-black/50" />
    <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded-lg">
      <Dialog.Title>Title</Dialog.Title>
      <Dialog.Description>Description...</Dialog.Description>
      <Dialog.Close asChild>
        <button>Close</button>
      </Dialog.Close>
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

Dropdown Menu

A comprehensive dropdown menu with full keyboard support:

TScomponents/DropdownMenu.tsx
TypeScript
// components/DropdownMenu.tsx
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import {
  User,
  Settings,
  CreditCard,
  LogOut,
  ChevronRight,
  Check
} from 'lucide-react'

interface UserMenuProps {
  user: {
    name: string
    email: string
    avatar: string
  }
  onLogout: () => void
}

export function UserMenu({ user, onLogout }: UserMenuProps) {
  const [theme, setTheme] = useState('system')
  const [notifications, setNotifications] = useState(true)

  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <button className="flex items-center gap-2 p-2 rounded-full hover:bg-gray-100">
          <img
            src={user.avatar}
            alt={user.name}
            className="w-8 h-8 rounded-full"
          />
        </button>
      </DropdownMenu.Trigger>

      <DropdownMenu.Portal>
        <DropdownMenu.Content
          className="min-w-[220px] bg-white dark:bg-gray-900
            rounded-lg shadow-lg border border-gray-200 dark:border-gray-800
            p-1.5 animate-in fade-in-0 zoom-in-95"
          sideOffset={8}
          align="end"
        >
          {/* Header with user information */}
          <DropdownMenu.Label className="px-2 py-1.5">
            <p className="font-medium">{user.name}</p>
            <p className="text-sm text-gray-500">{user.email}</p>
          </DropdownMenu.Label>

          <DropdownMenu.Separator className="h-px bg-gray-200 dark:bg-gray-800 my-1" />

          {/* Regular menu items */}
          <DropdownMenu.Item
            className="flex items-center gap-2 px-2 py-1.5 rounded
              cursor-pointer outline-none
              hover:bg-gray-100 dark:hover:bg-gray-800
              focus:bg-gray-100 dark:focus:bg-gray-800"
          >
            <User className="w-4 h-4" />
            <span>Profile</span>
          </DropdownMenu.Item>

          <DropdownMenu.Item className="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer outline-none hover:bg-gray-100 focus:bg-gray-100">
            <CreditCard className="w-4 h-4" />
            <span>Billing</span>
          </DropdownMenu.Item>

          <DropdownMenu.Item className="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer outline-none hover:bg-gray-100 focus:bg-gray-100">
            <Settings className="w-4 h-4" />
            <span>Settings</span>
          </DropdownMenu.Item>

          <DropdownMenu.Separator className="h-px bg-gray-200 my-1" />

          {/* Submenu */}
          <DropdownMenu.Sub>
            <DropdownMenu.SubTrigger
              className="flex items-center justify-between px-2 py-1.5 rounded
                cursor-pointer outline-none
                hover:bg-gray-100 focus:bg-gray-100
                data-[state=open]:bg-gray-100"
            >
              <span>Theme</span>
              <ChevronRight className="w-4 h-4" />
            </DropdownMenu.SubTrigger>

            <DropdownMenu.Portal>
              <DropdownMenu.SubContent
                className="min-w-[160px] bg-white rounded-lg shadow-lg border p-1.5"
                sideOffset={4}
              >
                <DropdownMenu.RadioGroup value={theme} onValueChange={setTheme}>
                  <DropdownMenu.RadioItem
                    value="light"
                    className="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer outline-none hover:bg-gray-100"
                  >
                    <DropdownMenu.ItemIndicator>
                      <Check className="w-4 h-4" />
                    </DropdownMenu.ItemIndicator>
                    <span className="pl-6">Light</span>
                  </DropdownMenu.RadioItem>

                  <DropdownMenu.RadioItem
                    value="dark"
                    className="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer outline-none hover:bg-gray-100"
                  >
                    <DropdownMenu.ItemIndicator>
                      <Check className="w-4 h-4" />
                    </DropdownMenu.ItemIndicator>
                    <span className="pl-6">Dark</span>
                  </DropdownMenu.RadioItem>

                  <DropdownMenu.RadioItem
                    value="system"
                    className="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer outline-none hover:bg-gray-100"
                  >
                    <DropdownMenu.ItemIndicator>
                      <Check className="w-4 h-4" />
                    </DropdownMenu.ItemIndicator>
                    <span className="pl-6">System</span>
                  </DropdownMenu.RadioItem>
                </DropdownMenu.RadioGroup>
              </DropdownMenu.SubContent>
            </DropdownMenu.Portal>
          </DropdownMenu.Sub>

          {/* Checkbox item */}
          <DropdownMenu.CheckboxItem
            checked={notifications}
            onCheckedChange={setNotifications}
            className="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer outline-none hover:bg-gray-100"
          >
            <DropdownMenu.ItemIndicator>
              <Check className="w-4 h-4" />
            </DropdownMenu.ItemIndicator>
            <span className="pl-6">Notifications</span>
          </DropdownMenu.CheckboxItem>

          <DropdownMenu.Separator className="h-px bg-gray-200 my-1" />

          {/* Logout with custom styling */}
          <DropdownMenu.Item
            onClick={onLogout}
            className="flex items-center gap-2 px-2 py-1.5 rounded
              cursor-pointer outline-none
              text-red-600 hover:bg-red-50 focus:bg-red-50"
          >
            <LogOut className="w-4 h-4" />
            <span>Log out</span>
          </DropdownMenu.Item>

          {/* Arrow pointing to the trigger */}
          <DropdownMenu.Arrow className="fill-white" />
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}

Popover

Ideal for tooltips, inline editing, and floating panels:

TScomponents/Popover.tsx
TypeScript
// components/Popover.tsx
import * as Popover from '@radix-ui/react-popover'
import { Settings, X } from 'lucide-react'

export function SettingsPopover() {
  return (
    <Popover.Root>
      <Popover.Trigger asChild>
        <button
          className="p-2 rounded-lg hover:bg-gray-100"
          aria-label="Settings"
        >
          <Settings className="w-5 h-5" />
        </button>
      </Popover.Trigger>

      <Popover.Portal>
        <Popover.Content
          className="w-80 bg-white dark:bg-gray-900 rounded-lg shadow-xl
            border border-gray-200 dark:border-gray-800 p-4
            animate-in fade-in-0 zoom-in-95
            data-[side=bottom]:slide-in-from-top-2
            data-[side=left]:slide-in-from-right-2
            data-[side=right]:slide-in-from-left-2
            data-[side=top]:slide-in-from-bottom-2"
          sideOffset={8}
        >
          <div className="flex justify-between items-center mb-4">
            <h3 className="font-semibold">Display settings</h3>
            <Popover.Close asChild>
              <button className="p-1 rounded hover:bg-gray-100">
                <X className="w-4 h-4" />
              </button>
            </Popover.Close>
          </div>

          <div className="space-y-4">
            <div>
              <label className="block text-sm font-medium mb-1">
                Text size
              </label>
              <select className="w-full px-3 py-2 border rounded-lg">
                <option>Small</option>
                <option>Medium</option>
                <option>Large</option>
              </select>
            </div>

            <div>
              <label className="block text-sm font-medium mb-1">
                Contrast
              </label>
              <input
                type="range"
                min="0"
                max="100"
                className="w-full"
              />
            </div>

            <div className="flex items-center justify-between">
              <span className="text-sm">Dark mode</span>
              <Switch />
            </div>
          </div>

          <Popover.Arrow className="fill-white" />
        </Popover.Content>
      </Popover.Portal>
    </Popover.Root>
  )
}

Tooltip

Lightweight tooltips with full accessibility support:

TScomponents/Tooltip.tsx
TypeScript
// components/Tooltip.tsx
import * as Tooltip from '@radix-ui/react-tooltip'

interface TooltipWrapperProps {
  content: string
  children: React.ReactNode
  side?: 'top' | 'right' | 'bottom' | 'left'
  delayDuration?: number
}

export function TooltipWrapper({
  content,
  children,
  side = 'top',
  delayDuration = 200,
}: TooltipWrapperProps) {
  return (
    <Tooltip.Provider delayDuration={delayDuration}>
      <Tooltip.Root>
        <Tooltip.Trigger asChild>
          {children}
        </Tooltip.Trigger>

        <Tooltip.Portal>
          <Tooltip.Content
            side={side}
            className="px-3 py-1.5 bg-gray-900 text-white text-sm rounded-lg
              animate-in fade-in-0 zoom-in-95
              data-[side=bottom]:slide-in-from-top-2
              data-[side=left]:slide-in-from-right-2
              data-[side=right]:slide-in-from-left-2
              data-[side=top]:slide-in-from-bottom-2"
            sideOffset={4}
          >
            {content}
            <Tooltip.Arrow className="fill-gray-900" />
          </Tooltip.Content>
        </Tooltip.Portal>
      </Tooltip.Root>
    </Tooltip.Provider>
  )
}

// Usage
function Example() {
  return (
    <TooltipWrapper content="Click to copy">
      <button className="p-2 rounded hover:bg-gray-100">
        <Copy className="w-5 h-5" />
      </button>
    </TooltipWrapper>
  )
}

Application-level Provider

TSapp/providers.tsx
TypeScript
// app/providers.tsx
import * as Tooltip from '@radix-ui/react-tooltip'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <Tooltip.Provider delayDuration={200} skipDelayDuration={500}>
      {children}
    </Tooltip.Provider>
  )
}

Tabs

Tabs with full keyboard support (arrow keys, Home, End):

TScomponents/Tabs.tsx
TypeScript
// components/Tabs.tsx
import * as Tabs from '@radix-ui/react-tabs'

interface Tab {
  value: string
  label: string
  content: React.ReactNode
  disabled?: boolean
}

interface TabsProps {
  tabs: Tab[]
  defaultValue?: string
}

export function TabsComponent({ tabs, defaultValue }: TabsProps) {
  return (
    <Tabs.Root
      defaultValue={defaultValue || tabs[0]?.value}
      className="w-full"
    >
      <Tabs.List
        className="flex border-b border-gray-200"
        aria-label="Manage account"
      >
        {tabs.map((tab) => (
          <Tabs.Trigger
            key={tab.value}
            value={tab.value}
            disabled={tab.disabled}
            className="px-4 py-2 text-sm font-medium
              border-b-2 border-transparent
              text-gray-500 hover:text-gray-700
              data-[state=active]:border-blue-500
              data-[state=active]:text-blue-600
              disabled:opacity-50 disabled:cursor-not-allowed
              focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
          >
            {tab.label}
          </Tabs.Trigger>
        ))}
      </Tabs.List>

      {tabs.map((tab) => (
        <Tabs.Content
          key={tab.value}
          value={tab.value}
          className="pt-4 focus:outline-none"
        >
          {tab.content}
        </Tabs.Content>
      ))}
    </Tabs.Root>
  )
}

// Usage
function AccountSettings() {
  const tabs = [
    {
      value: 'profile',
      label: 'Profile',
      content: <ProfileForm />,
    },
    {
      value: 'security',
      label: 'Security',
      content: <SecuritySettings />,
    },
    {
      value: 'notifications',
      label: 'Notifications',
      content: <NotificationSettings />,
    },
    {
      value: 'billing',
      label: 'Billing',
      content: <BillingInfo />,
      disabled: true, // Unavailable
    },
  ]

  return <TabsComponent tabs={tabs} defaultValue="profile" />
}

Vertical Tabs

Code
TypeScript
<Tabs.Root
  defaultValue="tab1"
  orientation="vertical"
  className="flex gap-4"
>
  <Tabs.List className="flex flex-col w-48 border-r">
    <Tabs.Trigger value="tab1" className="px-4 py-2 text-left data-[state=active]:bg-gray-100">
      Tab 1
    </Tabs.Trigger>
    <Tabs.Trigger value="tab2" className="px-4 py-2 text-left data-[state=active]:bg-gray-100">
      Tab 2
    </Tabs.Trigger>
  </Tabs.List>

  <div className="flex-1">
    <Tabs.Content value="tab1">Content 1</Tabs.Content>
    <Tabs.Content value="tab2">Content 2</Tabs.Content>
  </div>
</Tabs.Root>

Accordion

Collapsible sections with animations:

TScomponents/Accordion.tsx
TypeScript
// components/Accordion.tsx
import * as Accordion from '@radix-ui/react-accordion'
import { ChevronDown } from 'lucide-react'

interface AccordionItem {
  value: string
  trigger: string
  content: React.ReactNode
}

interface AccordionProps {
  items: AccordionItem[]
  type?: 'single' | 'multiple'
  defaultValue?: string | string[]
}

export function AccordionComponent({
  items,
  type = 'single',
  defaultValue,
}: AccordionProps) {
  return (
    <Accordion.Root
      type={type}
      defaultValue={defaultValue}
      collapsible={type === 'single'}
      className="w-full"
    >
      {items.map((item) => (
        <Accordion.Item
          key={item.value}
          value={item.value}
          className="border-b border-gray-200"
        >
          <Accordion.Header>
            <Accordion.Trigger
              className="flex w-full items-center justify-between
                py-4 font-medium text-left
                hover:underline
                [&[data-state=open]>svg]:rotate-180"
            >
              {item.trigger}
              <ChevronDown
                className="w-5 h-5 text-gray-500 transition-transform duration-200"
              />
            </Accordion.Trigger>
          </Accordion.Header>

          <Accordion.Content
            className="overflow-hidden
              data-[state=closed]:animate-accordion-up
              data-[state=open]:animate-accordion-down"
          >
            <div className="pb-4 text-gray-600">
              {item.content}
            </div>
          </Accordion.Content>
        </Accordion.Item>
      ))}
    </Accordion.Root>
  )
}

// Tailwind config for animations
// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      keyframes: {
        'accordion-down': {
          from: { height: '0' },
          to: { height: 'var(--radix-accordion-content-height)' },
        },
        'accordion-up': {
          from: { height: 'var(--radix-accordion-content-height)' },
          to: { height: '0' },
        },
      },
      animation: {
        'accordion-down': 'accordion-down 0.2s ease-out',
        'accordion-up': 'accordion-up 0.2s ease-out',
      },
    },
  },
}

Select

A full select with search, groups, and keyboard navigation:

TScomponents/Select.tsx
TypeScript
// components/Select.tsx
import * as Select from '@radix-ui/react-select'
import { Check, ChevronDown, ChevronUp } from 'lucide-react'

interface SelectOption {
  value: string
  label: string
  disabled?: boolean
}

interface SelectGroup {
  label: string
  options: SelectOption[]
}

interface SelectProps {
  placeholder?: string
  value?: string
  onValueChange: (value: string) => void
  groups: SelectGroup[]
}

export function SelectComponent({
  placeholder = 'Select...',
  value,
  onValueChange,
  groups,
}: SelectProps) {
  return (
    <Select.Root value={value} onValueChange={onValueChange}>
      <Select.Trigger
        className="flex items-center justify-between w-full px-3 py-2
          border border-gray-300 rounded-lg bg-white
          focus:outline-none focus:ring-2 focus:ring-blue-500
          data-[placeholder]:text-gray-400"
      >
        <Select.Value placeholder={placeholder} />
        <Select.Icon>
          <ChevronDown className="w-4 h-4 text-gray-500" />
        </Select.Icon>
      </Select.Trigger>

      <Select.Portal>
        <Select.Content
          className="overflow-hidden bg-white rounded-lg shadow-lg border
            animate-in fade-in-0 zoom-in-95"
          position="popper"
          sideOffset={4}
        >
          <Select.ScrollUpButton className="flex items-center justify-center h-6 bg-white">
            <ChevronUp className="w-4 h-4" />
          </Select.ScrollUpButton>

          <Select.Viewport className="p-1">
            {groups.map((group, groupIndex) => (
              <Select.Group key={group.label}>
                {groupIndex > 0 && (
                  <Select.Separator className="h-px bg-gray-200 my-1" />
                )}

                <Select.Label className="px-2 py-1.5 text-xs font-medium text-gray-500">
                  {group.label}
                </Select.Label>

                {group.options.map((option) => (
                  <Select.Item
                    key={option.value}
                    value={option.value}
                    disabled={option.disabled}
                    className="relative flex items-center px-8 py-2 rounded
                      cursor-pointer outline-none
                      hover:bg-gray-100 focus:bg-gray-100
                      data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed"
                  >
                    <Select.ItemIndicator className="absolute left-2">
                      <Check className="w-4 h-4" />
                    </Select.ItemIndicator>
                    <Select.ItemText>{option.label}</Select.ItemText>
                  </Select.Item>
                ))}
              </Select.Group>
            ))}
          </Select.Viewport>

          <Select.ScrollDownButton className="flex items-center justify-center h-6 bg-white">
            <ChevronDown className="w-4 h-4" />
          </Select.ScrollDownButton>
        </Select.Content>
      </Select.Portal>
    </Select.Root>
  )
}

// Usage
function CountrySelect() {
  const [country, setCountry] = useState('')

  const groups = [
    {
      label: 'Europe',
      options: [
        { value: 'pl', label: 'Poland' },
        { value: 'de', label: 'Germany' },
        { value: 'fr', label: 'France' },
      ],
    },
    {
      label: 'America',
      options: [
        { value: 'us', label: 'United States' },
        { value: 'ca', label: 'Canada' },
      ],
    },
  ]

  return (
    <SelectComponent
      placeholder="Select country"
      value={country}
      onValueChange={setCountry}
      groups={groups}
    />
  )
}

Switch

Toggle switch with animation:

TScomponents/Switch.tsx
TypeScript
// components/Switch.tsx
import * as Switch from '@radix-ui/react-switch'

interface SwitchProps {
  checked: boolean
  onCheckedChange: (checked: boolean) => void
  label?: string
  disabled?: boolean
}

export function SwitchComponent({
  checked,
  onCheckedChange,
  label,
  disabled = false,
}: SwitchProps) {
  return (
    <div className="flex items-center gap-3">
      <Switch.Root
        checked={checked}
        onCheckedChange={onCheckedChange}
        disabled={disabled}
        className="w-11 h-6 bg-gray-200 rounded-full
          relative cursor-pointer
          data-[state=checked]:bg-blue-500
          disabled:opacity-50 disabled:cursor-not-allowed
          focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
      >
        <Switch.Thumb
          className="block w-5 h-5 bg-white rounded-full shadow
            transition-transform duration-200
            translate-x-0.5 data-[state=checked]:translate-x-[22px]"
        />
      </Switch.Root>

      {label && (
        <label className="text-sm font-medium">
          {label}
        </label>
      )}
    </div>
  )
}

// Usage
function NotificationSettings() {
  const [emailNotifications, setEmailNotifications] = useState(true)
  const [pushNotifications, setPushNotifications] = useState(false)

  return (
    <div className="space-y-4">
      <SwitchComponent
        checked={emailNotifications}
        onCheckedChange={setEmailNotifications}
        label="Email notifications"
      />

      <SwitchComponent
        checked={pushNotifications}
        onCheckedChange={setPushNotifications}
        label="Push notifications"
      />
    </div>
  )
}

Checkbox

Checkbox with indeterminate state:

TScomponents/Checkbox.tsx
TypeScript
// components/Checkbox.tsx
import * as Checkbox from '@radix-ui/react-checkbox'
import { Check, Minus } from 'lucide-react'

interface CheckboxProps {
  checked: boolean | 'indeterminate'
  onCheckedChange: (checked: boolean | 'indeterminate') => void
  label: string
  description?: string
}

export function CheckboxComponent({
  checked,
  onCheckedChange,
  label,
  description,
}: CheckboxProps) {
  return (
    <div className="flex items-start gap-3">
      <Checkbox.Root
        checked={checked}
        onCheckedChange={onCheckedChange}
        className="w-5 h-5 border-2 border-gray-300 rounded
          flex items-center justify-center
          data-[state=checked]:bg-blue-500 data-[state=checked]:border-blue-500
          data-[state=indeterminate]:bg-blue-500 data-[state=indeterminate]:border-blue-500
          focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
      >
        <Checkbox.Indicator>
          {checked === 'indeterminate' ? (
            <Minus className="w-3 h-3 text-white" />
          ) : (
            <Check className="w-3 h-3 text-white" />
          )}
        </Checkbox.Indicator>
      </Checkbox.Root>

      <div>
        <label className="text-sm font-medium cursor-pointer">
          {label}
        </label>
        {description && (
          <p className="text-sm text-gray-500">{description}</p>
        )}
      </div>
    </div>
  )
}

// List with "Select All"
function CheckboxList() {
  const [items, setItems] = useState([
    { id: '1', label: 'Item 1', checked: false },
    { id: '2', label: 'Item 2', checked: true },
    { id: '3', label: 'Item 3', checked: false },
  ])

  const allChecked = items.every(item => item.checked)
  const someChecked = items.some(item => item.checked)
  const selectAllState = allChecked ? true : someChecked ? 'indeterminate' : false

  const handleSelectAll = (checked: boolean | 'indeterminate') => {
    if (checked === 'indeterminate') return
    setItems(items.map(item => ({ ...item, checked })))
  }

  const handleItemChange = (id: string, checked: boolean | 'indeterminate') => {
    if (checked === 'indeterminate') return
    setItems(items.map(item =>
      item.id === id ? { ...item, checked } : item
    ))
  }

  return (
    <div className="space-y-3">
      <CheckboxComponent
        checked={selectAllState}
        onCheckedChange={handleSelectAll}
        label="Select all"
      />

      <div className="ml-6 space-y-2">
        {items.map(item => (
          <CheckboxComponent
            key={item.id}
            checked={item.checked}
            onCheckedChange={(checked) => handleItemChange(item.id, checked)}
            label={item.label}
          />
        ))}
      </div>
    </div>
  )
}

Slider

Slider with multiple values:

TScomponents/Slider.tsx
TypeScript
// components/Slider.tsx
import * as Slider from '@radix-ui/react-slider'

interface SliderProps {
  value: number[]
  onValueChange: (value: number[]) => void
  min?: number
  max?: number
  step?: number
  label?: string
}

export function SliderComponent({
  value,
  onValueChange,
  min = 0,
  max = 100,
  step = 1,
  label,
}: SliderProps) {
  return (
    <div className="w-full">
      {label && (
        <div className="flex justify-between mb-2">
          <span className="text-sm font-medium">{label}</span>
          <span className="text-sm text-gray-500">{value.join(' - ')}</span>
        </div>
      )}

      <Slider.Root
        value={value}
        onValueChange={onValueChange}
        min={min}
        max={max}
        step={step}
        className="relative flex items-center w-full h-5 select-none"
      >
        <Slider.Track className="relative h-2 w-full grow rounded-full bg-gray-200">
          <Slider.Range className="absolute h-full rounded-full bg-blue-500" />
        </Slider.Track>

        {value.map((_, index) => (
          <Slider.Thumb
            key={index}
            className="block w-5 h-5 bg-white border-2 border-blue-500 rounded-full
              shadow focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
              hover:bg-blue-50"
          />
        ))}
      </Slider.Root>
    </div>
  )
}

// Usage - single slider
function VolumeControl() {
  const [volume, setVolume] = useState([50])

  return (
    <SliderComponent
      value={volume}
      onValueChange={setVolume}
      label="Volume"
    />
  )
}

// Usage - range
function PriceRange() {
  const [range, setRange] = useState([20, 80])

  return (
    <SliderComponent
      value={range}
      onValueChange={setRange}
      min={0}
      max={1000}
      step={10}
      label="Price range (USD)"
    />
  )
}

Navigation Menu

Advanced navigation menu (like on large websites):

TScomponents/NavigationMenu.tsx
TypeScript
// components/NavigationMenu.tsx
import * as NavigationMenu from '@radix-ui/react-navigation-menu'
import { ChevronDown } from 'lucide-react'

export function MainNavigation() {
  return (
    <NavigationMenu.Root className="relative z-10">
      <NavigationMenu.List className="flex items-center gap-1 p-1">
        {/* Link without submenu */}
        <NavigationMenu.Item>
          <NavigationMenu.Link
            href="/"
            className="px-3 py-2 rounded-md text-sm font-medium
              hover:bg-gray-100 focus:outline-none focus:ring-2"
          >
            Home
          </NavigationMenu.Link>
        </NavigationMenu.Item>

        {/* Products with mega menu */}
        <NavigationMenu.Item>
          <NavigationMenu.Trigger
            className="flex items-center gap-1 px-3 py-2 rounded-md text-sm font-medium
              hover:bg-gray-100 focus:outline-none focus:ring-2
              data-[state=open]:bg-gray-100"
          >
            Products
            <ChevronDown className="w-4 h-4 transition-transform data-[state=open]:rotate-180" />
          </NavigationMenu.Trigger>

          <NavigationMenu.Content
            className="absolute top-full left-0 w-full
              data-[motion=from-start]:animate-enterFromLeft
              data-[motion=from-end]:animate-enterFromRight
              data-[motion=to-start]:animate-exitToLeft
              data-[motion=to-end]:animate-exitToRight"
          >
            <div className="grid grid-cols-3 gap-4 p-6 bg-white shadow-lg rounded-lg border">
              <div>
                <h3 className="font-semibold mb-3">For Developers</h3>
                <ul className="space-y-2">
                  <li><a href="/api" className="text-sm hover:text-blue-500">API</a></li>
                  <li><a href="/sdk" className="text-sm hover:text-blue-500">SDK</a></li>
                  <li><a href="/docs" className="text-sm hover:text-blue-500">Documentation</a></li>
                </ul>
              </div>

              <div>
                <h3 className="font-semibold mb-3">For Business</h3>
                <ul className="space-y-2">
                  <li><a href="/enterprise" className="text-sm hover:text-blue-500">Enterprise</a></li>
                  <li><a href="/pricing" className="text-sm hover:text-blue-500">Pricing</a></li>
                  <li><a href="/case-studies" className="text-sm hover:text-blue-500">Case Studies</a></li>
                </ul>
              </div>

              <div className="bg-gray-50 rounded-lg p-4">
                <h3 className="font-semibold mb-2">New!</h3>
                <p className="text-sm text-gray-600 mb-3">
                  Check out our latest product - AI Assistant.
                </p>
                <a
                  href="/ai-assistant"
                  className="text-sm text-blue-500 font-medium"
                >
                  Learn more →
                </a>
              </div>
            </div>
          </NavigationMenu.Content>
        </NavigationMenu.Item>

        {/* Resources with list */}
        <NavigationMenu.Item>
          <NavigationMenu.Trigger
            className="flex items-center gap-1 px-3 py-2 rounded-md text-sm font-medium
              hover:bg-gray-100"
          >
            Resources
            <ChevronDown className="w-4 h-4" />
          </NavigationMenu.Trigger>

          <NavigationMenu.Content className="absolute top-full left-0">
            <ul className="w-48 p-2 bg-white shadow-lg rounded-lg border">
              <li>
                <NavigationMenu.Link
                  href="/blog"
                  className="block px-3 py-2 rounded hover:bg-gray-100 text-sm"
                >
                  Blog
                </NavigationMenu.Link>
              </li>
              <li>
                <NavigationMenu.Link
                  href="/tutorials"
                  className="block px-3 py-2 rounded hover:bg-gray-100 text-sm"
                >
                  Tutorials
                </NavigationMenu.Link>
              </li>
              <li>
                <NavigationMenu.Link
                  href="/changelog"
                  className="block px-3 py-2 rounded hover:bg-gray-100 text-sm"
                >
                  Changelog
                </NavigationMenu.Link>
              </li>
            </ul>
          </NavigationMenu.Content>
        </NavigationMenu.Item>

        <NavigationMenu.Indicator
          className="top-full z-10 flex h-2.5 items-end justify-center overflow-hidden
            data-[state=visible]:animate-in data-[state=visible]:fade-in
            data-[state=hidden]:animate-out data-[state=hidden]:fade-out"
        >
          <div className="relative top-1/2 h-2 w-2 rotate-45 bg-white border" />
        </NavigationMenu.Indicator>
      </NavigationMenu.List>

      <NavigationMenu.Viewport
        className="absolute top-full left-0 flex w-full justify-center"
      />
    </NavigationMenu.Root>
  )
}

Context Menu

Context menu (right-click):

TScomponents/ContextMenu.tsx
TypeScript
// components/ContextMenu.tsx
import * as ContextMenu from '@radix-ui/react-context-menu'
import { Copy, Trash, Edit, Share } from 'lucide-react'

interface FileContextMenuProps {
  children: React.ReactNode
  onCopy: () => void
  onEdit: () => void
  onDelete: () => void
  onShare: () => void
}

export function FileContextMenu({
  children,
  onCopy,
  onEdit,
  onDelete,
  onShare,
}: FileContextMenuProps) {
  return (
    <ContextMenu.Root>
      <ContextMenu.Trigger asChild>
        {children}
      </ContextMenu.Trigger>

      <ContextMenu.Portal>
        <ContextMenu.Content
          className="min-w-[180px] bg-white rounded-lg shadow-lg border p-1
            animate-in fade-in-0 zoom-in-95"
        >
          <ContextMenu.Item
            onClick={onCopy}
            className="flex items-center gap-2 px-2 py-1.5 rounded
              cursor-pointer outline-none hover:bg-gray-100"
          >
            <Copy className="w-4 h-4" />
            Copy
            <ContextMenu.Shortcut className="ml-auto text-xs text-gray-400">
C
            </ContextMenu.Shortcut>
          </ContextMenu.Item>

          <ContextMenu.Item
            onClick={onEdit}
            className="flex items-center gap-2 px-2 py-1.5 rounded
              cursor-pointer outline-none hover:bg-gray-100"
          >
            <Edit className="w-4 h-4" />
            Edit
            <ContextMenu.Shortcut className="ml-auto text-xs text-gray-400">
E
            </ContextMenu.Shortcut>
          </ContextMenu.Item>

          <ContextMenu.Item
            onClick={onShare}
            className="flex items-center gap-2 px-2 py-1.5 rounded
              cursor-pointer outline-none hover:bg-gray-100"
          >
            <Share className="w-4 h-4" />
            Share
          </ContextMenu.Item>

          <ContextMenu.Separator className="h-px bg-gray-200 my-1" />

          <ContextMenu.Item
            onClick={onDelete}
            className="flex items-center gap-2 px-2 py-1.5 rounded
              cursor-pointer outline-none text-red-600 hover:bg-red-50"
          >
            <Trash className="w-4 h-4" />
            Delete
            <ContextMenu.Shortcut className="ml-auto text-xs">
            </ContextMenu.Shortcut>
          </ContextMenu.Item>
        </ContextMenu.Content>
      </ContextMenu.Portal>
    </ContextMenu.Root>
  )
}

// Usage
function FileItem({ file }: { file: File }) {
  return (
    <FileContextMenu
      onCopy={() => console.log('Copy', file.name)}
      onEdit={() => console.log('Edit', file.name)}
      onDelete={() => console.log('Delete', file.name)}
      onShare={() => console.log('Share', file.name)}
    >
      <div className="p-4 border rounded hover:bg-gray-50 cursor-pointer">
        <span>{file.name}</span>
      </div>
    </FileContextMenu>
  )
}

Alert Dialog

A confirmation dialog for destructive actions:

TScomponents/AlertDialog.tsx
TypeScript
// components/AlertDialog.tsx
import * as AlertDialog from '@radix-ui/react-alert-dialog'

interface ConfirmDialogProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  title: string
  description: string
  confirmLabel?: string
  cancelLabel?: string
  onConfirm: () => void
  variant?: 'danger' | 'warning'
}

export function ConfirmDialog({
  open,
  onOpenChange,
  title,
  description,
  confirmLabel = 'Confirm',
  cancelLabel = 'Cancel',
  onConfirm,
  variant = 'danger',
}: ConfirmDialogProps) {
  const confirmButtonClass = variant === 'danger'
    ? 'bg-red-500 hover:bg-red-600 text-white'
    : 'bg-yellow-500 hover:bg-yellow-600 text-white'

  return (
    <AlertDialog.Root open={open} onOpenChange={onOpenChange}>
      <AlertDialog.Portal>
        <AlertDialog.Overlay
          className="fixed inset-0 bg-black/50 backdrop-blur-sm
            data-[state=open]:animate-in data-[state=closed]:animate-out
            data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
        />

        <AlertDialog.Content
          className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2
            w-full max-w-md bg-white rounded-xl shadow-xl p-6
            data-[state=open]:animate-in data-[state=closed]:animate-out
            data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
            data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
        >
          <AlertDialog.Title className="text-xl font-semibold">
            {title}
          </AlertDialog.Title>

          <AlertDialog.Description className="mt-2 text-gray-500">
            {description}
          </AlertDialog.Description>

          <div className="flex justify-end gap-3 mt-6">
            <AlertDialog.Cancel asChild>
              <button className="px-4 py-2 rounded-lg border hover:bg-gray-50">
                {cancelLabel}
              </button>
            </AlertDialog.Cancel>

            <AlertDialog.Action asChild>
              <button
                onClick={onConfirm}
                className={`px-4 py-2 rounded-lg ${confirmButtonClass}`}
              >
                {confirmLabel}
              </button>
            </AlertDialog.Action>
          </div>
        </AlertDialog.Content>
      </AlertDialog.Portal>
    </AlertDialog.Root>
  )
}

// Usage
function DeleteButton() {
  const [open, setOpen] = useState(false)

  const handleDelete = () => {
    // Perform deletion
    console.log('Deleted!')
    setOpen(false)
  }

  return (
    <>
      <button
        onClick={() => setOpen(true)}
        className="px-4 py-2 bg-red-500 text-white rounded"
      >
        Delete account
      </button>

      <ConfirmDialog
        open={open}
        onOpenChange={setOpen}
        title="Are you sure you want to delete your account?"
        description="This action is irreversible. All your data will be permanently deleted."
        confirmLabel="Delete account"
        onConfirm={handleDelete}
        variant="danger"
      />
    </>
  )
}

Animations with Framer Motion

Radix works great with Framer Motion:

Code
TypeScript
import * as Dialog from '@radix-ui/react-dialog'
import { motion, AnimatePresence } from 'framer-motion'
import { forwardRef } from 'react'

// Motion wrapper for Radix
const MotionOverlay = motion(
  forwardRef<HTMLDivElement, Dialog.DialogOverlayProps>((props, ref) => (
    <Dialog.Overlay ref={ref} {...props} />
  ))
)

const MotionContent = motion(
  forwardRef<HTMLDivElement, Dialog.DialogContentProps>((props, ref) => (
    <Dialog.Content ref={ref} {...props} />
  ))
)

interface AnimatedDialogProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  children: React.ReactNode
}

export function AnimatedDialog({ open, onOpenChange, children }: AnimatedDialogProps) {
  return (
    <Dialog.Root open={open} onOpenChange={onOpenChange}>
      <AnimatePresence>
        {open && (
          <Dialog.Portal forceMount>
            <MotionOverlay
              className="fixed inset-0 bg-black/50"
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
            />

            <MotionContent
              className="fixed left-1/2 top-1/2 w-full max-w-lg bg-white rounded-xl p-6"
              initial={{
                opacity: 0,
                scale: 0.95,
                x: '-50%',
                y: '-50%',
              }}
              animate={{
                opacity: 1,
                scale: 1,
                x: '-50%',
                y: '-50%',
              }}
              exit={{
                opacity: 0,
                scale: 0.95,
                x: '-50%',
                y: '-50%',
              }}
              transition={{
                type: 'spring',
                damping: 25,
                stiffness: 300
              }}
            >
              {children}
            </MotionContent>
          </Dialog.Portal>
        )}
      </AnimatePresence>
    </Dialog.Root>
  )
}

Complete list of Radix primitives

ComponentDescriptionUsage
DialogModal/popupForms, confirmations
Alert DialogConfirmation dialogDestructive actions
Dropdown MenuDropdown menuUser actions
Context MenuContext menuRight-click
MenubarMenu barDesktop-like applications
Navigation MenuNavigation menuMain navigation
PopoverFloating panelInline editing, settings
Hover CardCard on hoverUser preview
TooltipTooltipUI explanations
AccordionCollapsible sectionsFAQ, lists
CollapsibleSingle sectionShow more/less
TabsTabsIn-view navigation
ToggleToggle buttonOn/off actions
Toggle GroupToggle groupRadio buttons
SelectDropdown selectForms
SliderSliderValue ranges
SwitchToggle switchBoolean settings
CheckboxCheckboxMulti-select
Radio GroupRadio buttonsSingle select
AvatarAvatarUser photo
ProgressProgress barLoading, progress
Scroll AreaScroll areaCustom scrollbars
SeparatorSeparatorVisual separation
ToolbarToolbarEditor actions
ToastNotificationsUser feedback
Aspect RatioAspect ratioMedia containers
FormFormForm validation
LabelLabelAccessibility labels
Visually HiddenHidden textScreen reader only

Summary

Radix UI is the foundation of modern React component libraries. Key advantages:

  • Full accessibility - WAI-ARIA compliant, keyboard navigation
  • Unstyled - 100% control over appearance
  • Composable API - Flexible UI building
  • Small sizes - Import only the components you need
  • SSR ready - Works with Next.js out of the box
  • Animations - Full support for Framer Motion and CSS

If you are building your own UI system or using Tailwind CSS, Radix UI is the ideal choice as the foundation for your components.