DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Picker

Labeled setting row that opens a check-marked option list — the compact control used by theme configurators, settings rails and editor strips.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Picker, PickerSkeleton } from "@tessinaui/ui";
{/* Basic — the trailing chevron is the default glyph */}
<Picker
  label="Style"
  options={[
    { value: "vega", label: "Vega" },
    { value: "nova", label: "Nova" },
    { value: "maia", label: "Maia" },
  ]}
  defaultValue="nova"
/>

{/* Controlled, with per-option adornments (the trigger falls back to the
    selected option's adornment when no trigger adornment is given) */}
<Picker
  label="Base Color"
  options={[
    { value: "violet", label: "Violet", adornment: <span className="size-3.5 rounded-full" style={{ background: "var(--chart-1)" }} /> },
    { value: "blue",   label: "Blue",   adornment: <span className="size-3.5 rounded-full" style={{ background: "var(--chart-2)" }} /> },
  ]}
  value={color}
  onValueChange={setColor}
/>

{/* Loading */}
<PickerSkeleton />

Stack pickers full-width in a sidebar on desktop, or give each a fixed width (e.g. w-36 shrink-0) inside a horizontally scrolling strip on mobile — setting rows stack flush (the row height is the rhythm).

When not to use a Picker

  • Searching or filtering a long list → Combobox. A Picker's list is known and short; past ~15 options you want typing.
  • A form field with helper/error text, label layouts or multi-select → Select. Picker's intent tints the boundary only, on purpose.
  • Commands and actions → DropdownMenu. Options describe state; menus do things (HIG's pop-up vs pull-down split).
  • Time/date wheels → TimePicker / native inputs.

Selection applies instantly and the popup closes on choose — a Picker is never behind an Apply button. Give it a sensible defaultValue: the placeholder is a hint, never the label. Provide option adornments for all options in a group or none of them, and order short lists semantically (most-used first) rather than alphabetically.

Examples

Default

Variants

Sizes

Rounded

Adornments

Option descriptions & badges

Grouped options

States & intents

On color

Loading & empty

Controlled

Self-previewing values

Popup footer

Mobile strip

Paired pickers

In a form

Accessibility

  • The trigger is a role="combobox" button named by the visible caption + value (aria-labelledby) — it announces as e.g. "Style Nova". Never add a duplicate aria-label.
  • The option list is a real role="listbox"; the popup shell around it is presentational, which is what makes popupFooter content legal.
  • emptyText/loadingText render as disabled option rows (a listbox must never own zero options); the list is aria-busy while loading.
  • Option description reaches AT via aria-describedby — the option's name stays its label. Rich label nodes need textValue for typeahead and announcements.
  • required renders a star plus screen-reader text and aria-required; intent="error" stamps aria-invalid and data-invalid.
  • Full keyboard: Space/Enter/ArrowUp/ArrowDown open; arrows + Home/End move; Space/Enter select; Esc closes back to the trigger; typeahead works on the closed trigger and the open list.
  • AT matrix: VoiceOver (macOS) verified locally; NVDA and JAWS are UNTESTED on this project's hardware.

API Reference

Picker props

PropTypeDefaultDescription
labelReactNode—Small muted caption above the value (e.g. "Style")
optionsPickerEntry[]—Options, { type: "group", label, options } groups and { type: "separator" } rows
valuestring | null—Controlled selected value; null shows the placeholder
defaultValuestring—Uncontrolled initial value
onValueChange(value: string, details) => void—Called on selection; details is Base UI's event details
adornmentReactNode | nullchevronTrailing glyph; falls back to the selected option's adornment, then a rotating chevron. null removes it
placeholderReactNode"Select…"Value text when nothing is selected
size"sm" | "md""md"Row padding; value text reads the input-family ladder (16px on phones)
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""md"§2 canonical scale; full caps at 24px (panel rule)
variant"outline" | "ghost""outline"Bordered well vs borderless settings-rail row
intent"none" | "error" | "warning" | "success" | "info""none"Boundary + ring tint; error adds aria-invalid/data-invalid. No text slot — compose helper text via Field/Form
tone"default" | "on-color"from SurfaceColored-plate rendering; resolves automatically inside <Surface>
disabledboolean—Removes the row from the tab order
readOnlyboolean—Focusable but the value cannot change; full-contrast ink
requiredboolean—Caption star + sr-only text + aria-required
namestring—Submits via the primitive's hidden input
open / defaultOpenboolean—Popup visibility (controlled / initial)
onOpenChange(open: boolean) => void—Popup visibility callback
loadingboolean—List shows loadingText and announces busy
loadingTextReactNode"Loading…"Row shown while loading
emptyTextReactNode"No options"Row shown when there are no options
dir"ltr" | "rtl"inheritStamped only when set — otherwise the ambient direction applies
side"top" | "bottom" | "left" | "right" | "inline-start" | "inline-end""bottom"Popup side; auto-flips when out of room
align"start" | "center" | "end""start"Popup alignment along the side
popupFooterReactNode—Content below the list, outside the listbox — buttons are legal ("Manage…", "Custom…")
classNamestring—Extra classes on the trigger
popupClassNamestring—Extra classes on the popup
positionerClassNamestring—Raise the positioner's z-50 inside higher stacking contexts

The trigger also accepts every native button attribute (id, data-*, aria-*, event handlers) — it is the control, so everything lands on it.

PickerOption

FieldTypeDescription
valuestringUnique option value
labelReactNodeOption text (also the trigger value when selected)
textValuestringTypeahead/announcement text for rich labels
descriptionstringSecond muted line; announced via aria-describedby
trailingTextReactNodeTrailing muted meta ("New", a price) — outside the name
adornmentReactNodeLeading visual in the list (e.g. a color swatch)
disabledbooleanDisables this option (description still explains why)

PickerSkeleton props

PropTypeDefaultDescription
size"sm" | "md""md"Matches the trigger footprint
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""md"Border radius
showAdornmentbooleantrueRender the trailing-glyph placeholder
classNamestring—Extra classes

Data attributes

data-component="picker" and data-slot on every part (picker-trigger/label/value/adornment/popup/list/item/item-description/item-trailing/item-indicator/group/group-label/separator/footer/loading/empty/skeleton), plus state marks: data-empty, data-invalid, data-variant, data-tone, and the primitive's data-popup-open/data-disabled/data-selected/data-highlighted.

PaginationPie Chart

On this page

PlaygroundInstallationUsageWhen not to use a PickerExamplesDefaultVariantsSizesRoundedAdornmentsOption descriptions & badgesGrouped optionsStates & intentsOn colorLoading & emptyControlledSelf-previewing valuesPopup footerMobile stripPaired pickersIn a formAccessibilityAPI ReferencePicker propsPickerOptionPickerSkeleton propsData attributes