Radix Primitives Reference

Radix primitives provide behavior for complex interactions while leaving the visual design to your app. Style every Radix part with Tailwind classes so the result matches the app's local design.

Overview

Use native HTML and local Tailwind components for ordinary UI such as buttons, cards, panels, inputs, badges, lists, and page structure.

Reach for direct Radix imports when the interaction itself is complex: dialogs, bottom sheets, side panels, alert dialogs, dropdown menus, popovers, tabs, selects, accordions, switches, sliders, radio groups, checkboxes, and tooltips.

The generated app runtime depends on the individual Radix React packages, so use direct @radix-ui/react-* imports even when upstream examples show the aggregate radix-ui package.

import * as Dialog from '@radix-ui/react-dialog'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import * as Tabs from '@radix-ui/react-tabs'

Style interactive states with Radix data attributes such as data-state, data-highlighted, data-disabled, data-side, data-align, and data-placeholder.

Dialog and Sheet

Use a bottom sheet for compact mobile tasks and a side panel for supporting details or editing. Both use @radix-ui/react-dialog: the default modal mode supplies focus containment, Escape and outside-click dismissal, and focus return to Dialog.Trigger.

Keep a visible Dialog.Close button and an accessible Dialog.Title. The examples focus the title on opening so a form does not immediately open the software keyboard. Keep the title inside the focus trap with tabIndex={-1}; do not prevent autofocus without moving focus into the panel.

These panels open and close from their anchored edge. They do not implement drag-to-dismiss, so omit a drag handle. Use motion-safe: for animation, dvh for viewport sizing, and safe-area padding for controls near screen edges. Keep the header outside the min-h-0 overflow-y-auto body so it remains reachable when content is long. Verify focused fields remain visible with the mobile keyboard open.

Bottom Sheet

This local component owns its open state and keeps a native field's value when the sheet closes. Compose real app fields and actions in the same scrollable body.

import { useId, useRef, useState } from 'react'
import * as Dialog from '@radix-ui/react-dialog'

function BottomSheet() {
  const [open, setOpen] = useState(false)
  const [weekStart, setWeekStart] = useState('monday')
  const titleRef = useRef<HTMLHeadingElement>(null)
  const weekStartId = useId()

  return (
    <Dialog.Root open={open} onOpenChange={setOpen}>
      <Dialog.Trigger className="min-h-11 rounded-full bg-zinc-950 px-4 py-2 text-sm font-medium text-white">
        Calendar settings
      </Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 z-40 bg-black/50 motion-safe:data-[state=open]:animate-in motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=closed]:fade-out-0 motion-safe:duration-200" />
        <Dialog.Content
          className="fixed inset-x-0 bottom-0 z-50 flex max-h-[90dvh] flex-col gap-4 rounded-t-3xl bg-white p-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] text-zinc-950 shadow-2xl motion-safe:data-[state=open]:animate-in motion-safe:data-[state=open]:slide-in-from-bottom motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=closed]:slide-out-to-bottom motion-safe:duration-200"
          onOpenAutoFocus={(event) => {
            event.preventDefault()
            titleRef.current?.focus({ preventScroll: true })
          }}
        >
          <div className="flex shrink-0 items-start justify-between gap-4">
            <div>
              <Dialog.Title
                ref={titleRef}
                tabIndex={-1}
                className="text-lg font-semibold outline-none"
              >
                Calendar settings
              </Dialog.Title>
              <Dialog.Description className="mt-1 text-sm text-zinc-600">
                Choose how your week is displayed.
              </Dialog.Description>
            </div>
            <Dialog.Close className="min-h-11 shrink-0 rounded-full border border-zinc-300 px-4 text-sm font-medium">
              Close
            </Dialog.Close>
          </div>
          <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
            <label htmlFor={weekStartId} className="block text-sm font-medium">
              Week starts on
            </label>
            <select
              id={weekStartId}
              value={weekStart}
              onChange={(event) => setWeekStart(event.target.value)}
              className="mt-2 min-h-11 w-full rounded-xl border border-zinc-400 bg-white px-3 text-base"
            >
              <option value="monday">Monday</option>
              <option value="sunday">Sunday</option>
            </select>
          </div>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  )
}

Side Panel

Use the same composition anchored to the right for a modal detail panel. The panel fills the viewport on narrow screens and caps its width on larger screens. Background content remains inert while it is open.

import { useRef, useState } from 'react'
import * as Dialog from '@radix-ui/react-dialog'

function SidePanel() {
  const [open, setOpen] = useState(false)
  const titleRef = useRef<HTMLHeadingElement>(null)

  return (
    <Dialog.Root open={open} onOpenChange={setOpen}>
      <Dialog.Trigger className="min-h-11 rounded-full bg-zinc-950 px-4 py-2 text-sm font-medium text-white">
        Project details
      </Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 z-40 bg-black/50 motion-safe:data-[state=open]:animate-in motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=closed]:fade-out-0 motion-safe:duration-200" />
        <Dialog.Content
          className="fixed right-0 top-0 z-50 flex h-dvh w-full max-w-md flex-col gap-4 bg-white p-6 pt-[max(1.5rem,env(safe-area-inset-top))] pr-[max(1.5rem,env(safe-area-inset-right))] pb-[max(1.5rem,env(safe-area-inset-bottom))] text-zinc-950 shadow-2xl motion-safe:data-[state=open]:animate-in motion-safe:data-[state=open]:slide-in-from-right motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=closed]:slide-out-to-right motion-safe:duration-200"
          onOpenAutoFocus={(event) => {
            event.preventDefault()
            titleRef.current?.focus({ preventScroll: true })
          }}
        >
          <div className="flex shrink-0 items-start justify-between gap-4">
            <div>
              <Dialog.Title
                ref={titleRef}
                tabIndex={-1}
                className="text-lg font-semibold outline-none"
              >
                Project details
              </Dialog.Title>
              <Dialog.Description className="mt-1 text-sm text-zinc-600">
                Review the current status and next step.
              </Dialog.Description>
            </div>
            <Dialog.Close className="min-h-11 shrink-0 rounded-full border border-zinc-300 px-4 text-sm font-medium">
              Close
            </Dialog.Close>
          </div>
          <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
            <dl className="space-y-4">
              <div>
                <dt className="text-sm text-zinc-600">Status</dt>
                <dd className="mt-1 font-medium">In progress</dd>
              </div>
              <div>
                <dt className="text-sm text-zinc-600">Next step</dt>
                <dd className="mt-1">Review the first draft.</dd>
              </div>
            </dl>
          </div>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  )
}

For a left panel, use left-0, left safe-area padding, and the matching slide-in-from-left / slide-out-to-left classes. Keep controlled state in the parent when another part of the app must open or close the panel.

Use dropdown menus for compact action lists and popovers for lightweight contextual panels.

import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { MoreHorizontal } from 'lucide-react'

<DropdownMenu.Root>
  <DropdownMenu.Trigger className="rounded-full p-2 text-zinc-500 hover:bg-zinc-100">
    <MoreHorizontal className="h-5 w-5" />
    <span className="sr-only">Open menu</span>
  </DropdownMenu.Trigger>
  <DropdownMenu.Portal>
    <DropdownMenu.Content
      align="end"
      sideOffset={8}
      className="z-50 min-w-44 rounded-2xl border border-zinc-200 bg-white p-1 text-sm text-zinc-900 shadow-xl"
    >
      <DropdownMenu.Item className="cursor-pointer rounded-xl px-3 py-2 outline-none data-[highlighted]:bg-zinc-100">
        Rename
      </DropdownMenu.Item>
      <DropdownMenu.Item className="cursor-pointer rounded-xl px-3 py-2 text-red-600 outline-none data-[highlighted]:bg-red-50">
        Delete
      </DropdownMenu.Item>
    </DropdownMenu.Content>
  </DropdownMenu.Portal>
</DropdownMenu.Root>

Tabs and Selects

Use tabs for meaningful view switching. Use Radix Select when native select is not enough for the app's visual design. Select items require Select.ItemText; use position="popper" when you want popover-style side, offset, and trigger-width positioning.

Every Select.Item must have a non-empty value. Do not create a placeholder item with value=""; Radix uses the empty string internally to clear the selection and show the placeholder. Use Select.Value placeholder="...", omit defaultValue for an initially empty select, or use a sentinel value such as "none" for a selectable None option.

import * as Tabs from '@radix-ui/react-tabs'

<Tabs.Root defaultValue="overview">
  <Tabs.List className="inline-flex rounded-full bg-zinc-100 p-1">
    <Tabs.Trigger
      value="overview"
      className="rounded-full px-4 py-2 text-sm font-medium text-zinc-500 data-[state=active]:bg-white data-[state=active]:text-zinc-950 data-[state=active]:shadow-sm"
    >
      Overview
    </Tabs.Trigger>
    <Tabs.Trigger
      value="history"
      className="rounded-full px-4 py-2 text-sm font-medium text-zinc-500 data-[state=active]:bg-white data-[state=active]:text-zinc-950 data-[state=active]:shadow-sm"
    >
      History
    </Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="overview" className="mt-4">
    Overview content
  </Tabs.Content>
  <Tabs.Content value="history" className="mt-4">
    History content
  </Tabs.Content>
</Tabs.Root>
import * as Select from '@radix-ui/react-select'
import { Check, ChevronDown } from 'lucide-react'

<Select.Root defaultValue="weekly">
  <Select.Trigger
    aria-label="Report cadence"
    className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white px-4 py-2 text-sm font-medium text-zinc-900 data-[placeholder]:text-zinc-400"
  >
    <Select.Value placeholder="Choose cadence" />
    <Select.Icon>
      <ChevronDown className="h-4 w-4" />
    </Select.Icon>
  </Select.Trigger>
  <Select.Portal>
    <Select.Content
      position="popper"
      sideOffset={8}
      className="z-50 w-[var(--radix-select-trigger-width)] overflow-hidden rounded-2xl border border-zinc-200 bg-white p-1 text-sm text-zinc-900 shadow-xl"
    >
      <Select.Viewport>
        <Select.Item
          value="daily"
          className="flex cursor-pointer items-center justify-between rounded-xl px-3 py-2 outline-none data-[highlighted]:bg-zinc-100"
        >
          <Select.ItemText>Daily</Select.ItemText>
          <Select.ItemIndicator>
            <Check className="h-4 w-4" />
          </Select.ItemIndicator>
        </Select.Item>
        <Select.Item
          value="weekly"
          className="flex cursor-pointer items-center justify-between rounded-xl px-3 py-2 outline-none data-[highlighted]:bg-zinc-100"
        >
          <Select.ItemText>Weekly</Select.ItemText>
          <Select.ItemIndicator>
            <Check className="h-4 w-4" />
          </Select.ItemIndicator>
        </Select.Item>
      </Select.Viewport>
    </Select.Content>
  </Select.Portal>
</Select.Root>

Control Primitives

Use these primitives when native controls cannot support the visual treatment.

  • @radix-ui/react-accordion: Root, Item, Header, Trigger, and Content; style open items with data-[state=open].
  • @radix-ui/react-checkbox: Root and Indicator; checked can be true, false, or 'indeterminate'.
  • @radix-ui/react-radio-group: Root, Item, and Indicator.
  • @radix-ui/react-switch: Root and Thumb; style data-[state=checked] and data-[state=unchecked].
  • @radix-ui/react-slider: Root, Track, Range, and one or more Thumb parts; values are number[].
  • @radix-ui/react-tooltip: wrap related tooltips in Tooltip.Provider.