Cheatsheet

Cheatsheet — Radix Primitives

Overlay/popup: which primitive?

NeedUseWhy
Modal blocking dialog, general contentDialogFocus-trapped, Esc closes, Title/Description announced
Modal requiring explicit confirm/cancel responseAlertDialogSame as Dialog but semantically an interrupt; Cancel/Action must be visually distinct
Non-modal rich content near a triggerPopoverDefaults non-modal (modal={false}); set modal={true} for Dialog-like trapping if needed
Click-triggered action listDropdownMenuPositions against Trigger
Right-click/long-press action listContextMenuPositions at pointer
Persistent File/Edit-style command barMenubarAdjacent-menu ArrowLeft/Right switching that plain DropdownMenus lack
Hierarchical site navigation with animated panel switchingNavigationMenuShared Viewport for cross-panel transitions
Short hint text, keyboard-accessibleTooltipOpens on focus AND hover
Rich preview, sighted-mouse-onlyHoverCardExplicitly excluded from a11y tree — never put essential-only content here

Selection: which primitive?

NeedUseWhy
Single choice, form-native, radio lookRadioGroupMutually exclusive, roving tabindex
Single choice, segmented-button lookToggleGroup type="single"Same exclusivity, button styling instead of dots
Independent multi-select togglesToggleGroup type="multiple" or several ToggleGrouped vs. fully independent
Boolean, form-checkbox semantics, optional indeterminateCheckboxTri-state (boolean | "indeterminate")
Boolean, immediate on/off setting feelSwitchTwo-state only, no indeterminate
Single value from a long/native-feeling listSelectItem-aligned positioning mimics native <select>

Decision rules

  • Use AlertDialog, not Dialog, when the interaction must interrupt and demand a response (destructive confirm) — Dialog doesn't guarantee the same focus/announcement contract for that use case.
  • Use forceMount only when a JS animation library needs to control unmount timing — CSS keyframes alone don't need it (Radix already suspends unmount for you).
  • Switch Select/Popover/etc. to "popper" positioning when the default item-aligned or trigger-adjacent placement collides with the viewport edge, or you need the same side/align API as the rest of your overlay UI.
  • Always pair Label (or Form.Label) with custom controls — none of the interactive primitives render their own visible label text.
  • Always render HiddenInput/rely on the default hidden input for Checkbox/RadioGroup/Switch/Slider used inside a real <form> — don't reach for the unstable_* decoupled parts unless you specifically need to relocate the input.
  • Wrap icon-only interactive elements in AccessibleIcon — a bare icon has no inherent accessible name.
  • Set Separator's decorative prop whenever the divider is purely visual, to avoid ARIA separator noise.

Accessibility tells & smells

  • If a component "just needs a right-click to open" → ContextMenu, not a manually-positioned Popover.
  • If a tooltip needs to work for keyboard-only users → Tooltip, never HoverCard.
  • If you're tracking open/checked/value state purely to add a CSS class → stop, use data-state instead.
  • If you're about to setTimeout an auto-dismiss notification → use Toast, it already handles duration, pause-on-hover/focus/blur, and swipe-dismiss.
  • If a custom component wrapped with asChild "loses" Radix's click/keyboard behavior → check it spreads props and forwards its ref.

Thresholds & defaults worth remembering

  • Tooltip/NavigationMenu default delayDuration: 700ms open delay (Tooltip), 200ms (NavigationMenu) — Tooltip's skipDelayDuration default 300ms.
  • HoverCard default openDelay/closeDelay: 700/300ms.
  • Toast default duration: 5000ms; default swipeThreshold: 50.
  • RadioGroup/ToggleGroup/Toolbar loop defaults to true.