Cheatsheet

Cheatsheet — Base UI

Overlay/popup: which component?

NeedUseWhy
Modal blocking dialog, general contentDialogFocus-trapped by default, Title/Description announced
Modal requiring explicit confirm/cancel responseAlertDialogSame anatomy as Dialog, semantically an interrupt
Edge-sliding panel needing swipe/snap pointsDrawerExtends Dialog with gestures — plain Dialog if no gesture need
Non-modal rich content near a triggerPopoverDefaults non-modal (modal={false})
Click-triggered action listMenuPositions against Trigger
Right-click/long-press action listContextMenuPositions at pointer — always pair with a visible control too
Persistent File/Edit-style command barMenubarCross-menu arrow-key switching plain Menus lack
Site nav with animated shared-panel dropdownsNavigationMenuShared Viewport for cross-panel transitions
Short hint text, keyboard-accessibleTooltipNever gate essential info here — touch/AT users can't reach it
Rich preview, sighted-mouse-onlyPreviewCardExplicitly excluded from a11y tree — content must be redundant with the link
Info-icon hover popupPopover with openOnHoverNot Tooltip — must stay reachable by touch/AT
Stacked/anchored notificationsToastImperative API (add/update/promise), not per-toast JSX

Form control: which component?

NeedUseWhy
Free-form text with optional suggestionsAutocompleteValue not constrained to the item set
Constrained selection, filterable (large list)ComboboxFilterable Select
Constrained selection, short list, no filterSelectKeyboard typeahead only
Single choice, form-native radio lookRadio + RadioGroupMutually exclusive, roving tabindex
Boolean, optional indeterminateCheckbox (+ CheckboxGroup for multi)Tri-state
Boolean, immediate on/off feelSwitchTwo-state only, no indeterminate
Numeric input with stepping/scrubNumberFieldLocale-aware formatting, drag-to-scrub
Fixed-length code entry (OTP/2FA)OTPFieldPaste-splitting, autoSubmit, autoComplete="one-time-code"
Plain textInputNo formatting/stepping overhead
Range/multi-thumb valueSliderminStepsBetweenValues for min/max pairs

Decision rules

  • Use AlertDialog, not Dialog, when the interaction must interrupt and demand a response — plain Dialog doesn't carry that semantic contract.
  • Use Drawer, not Dialog, when swipe-to-dismiss or snap points matter — otherwise Drawer's extra anatomy is unjustified.
  • Use Combobox, not Select, when the item list is long enough to need filtering; use Select for short lists where typeahead suffices.
  • Use Autocomplete, not Combobox, when free-form text outside the item set must be allowed.
  • Use Popover (openOnHover), never Tooltip, for "infotip" info-icon content — Tooltip is invisible to touch/AT users.
  • Set alignItemWithTrigger={false} on Select.Positioner the moment side/align seem to have no effect — the default overlap mode ignores them.
  • Always pair Fieldset.Root render={<GroupPrimitive/>} + Fieldset.Legend for any multi-control group (RadioGroup, CheckboxGroup, multi-thumb Slider) — a heading above the group isn't programmatically associated.
  • Always give each multi-thumb Slider.Thumb its own aria-label — the single Slider.Label only names the slider as a whole.
  • Use onValueCommitted, not onValueChange, for expensive side effects on NumberField/Slider — the latter fires continuously during drag/scrub.
  • Use event.preventBaseUIHandler() inside mergeProps, not eventDetails.cancel(), when intercepting a raw DOM/React event handler; use eventDetails.cancel() when intercepting a Base UI change event (onOpenChange, etc.).
  • Never render a link through Button — style the <a> directly; Button enforces conflicting button semantics.

Accessibility tells & smells

  • If a component "just needs a right-click to open" → ContextMenu, but ship a visible control for the same action too.
  • If a tooltip needs to work for keyboard/touch users → it's not a Tooltip, it's a Popover.
  • If you're tracking open/checked/value purely to add a CSS class → stop, use the matching data-* attribute instead.
  • If you're about to hand-roll an auto-dismiss notification → use Toast, it already handles stacking, swipe-dismiss, and promise-based lifecycle.
  • If a custom render-wrapped component "loses" Base UI's click/keyboard behavior → check it spreads props and forwards its ref.
  • If a Motion-animated popup won't unmount / unmounts too early → check opacity is part of the animation (even 0.9999) so getAnimations() detects completion.

Thresholds & defaults worth remembering

  • Field/form-control shared state attributes: data-valid/data-invalid, data-dirty, data-touched, data-filled, data-focused.
  • Form.validationMode default: onSubmit (revalidates invalid fields on change afterward).
  • Dialog/AlertDialog modal default: true. Popover modal default: false.
  • Select.Positioner.alignItemWithTrigger default: true (overrides side/align while active).
  • Accordion.multiple default: false (single-open).
  • mergeProps: prefer over mergePropsN for ≤5 prop sets; className concatenates rightmost-first, event handlers run rightmost-first.
  • useRender.defaultTagName default: 'div' when omitted.