DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
CheckboxRadioSwitchTextareaNumberFieldOTP InputSliderCircularSliderRatingSelectPickerComboboxSearchSearchSuggestTokenizerSegmentedControl
AI agentsContributing
  1. Components
  2. Picker

Picker

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

$ pnpm add @tessinaui/ui

When to use

  • One choice from a short, known list that fits a settings row: style, base colour, font, density.
  • Compact setting rows in a configurator, settings rail or editor strip, where a full form field with its own label and helper text would be too heavy.
  • A value that applies instantly. Selection applies and the popup closes on choose; a Picker is never behind an Apply button.
  • Options that benefit from a preview or a hint: a swatch or glyph (adornment), a second line (description) or a trailing note (trailingText).
  • A sensible defaultValue. The placeholder is a hint, never the label.

When not to use

  • Searching or filtering a long list. Use Combobox. A Picker's list is known and short; past ~15 options you want typing.
  • A form field with helper or error text, label layouts or multi-select. Use Select. Picker's intent tints the boundary only, on purpose.
  • Commands and actions. Use DropdownMenu. Options describe state; menus do things: a pop-up reports a value, a pull-down runs a command.
  • Time or date wheels. Use TimePicker or native inputs.
  • Two or three options that should all stay visible. Use Radio or SegmentedControl.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { Picker, PickerSkeleton } from "@tessinaui/ui";
example.tsx
{/* 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).

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

RTL

dir="rtl" mirrors the trigger (caption and value on the right, chevron on the left). align="start" then means the trigger's right edge, because the positioner receives dir too. Leave dir unset and the picker inherits the page's direction.

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…", Arabic under dir="rtl"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" | "filled" | "ghost""outline"Bordered well, a tinted well with no outline until focus or an intent, or the 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…", Arabic under dir="rtl"Row shown while loading
emptyTextReactNode"No options", Arabic under dir="rtl"Row shown when there are no options
dir"ltr" | "rtl"inheritStamped only when set — otherwise the ambient direction applies
stringsPartial<PickerStrings>English, Arabic under dir="rtl"Override the required caption text ("(required)") and the placeholder, loading and empty rows. placeholder, loadingText and emptyText win
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.

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 not yet run. Axe passes on the trigger at rest and on the open list (groups, separator, descriptions, footer, loading row) in both themes. NVDA and JAWS are untested (no Windows host).
SelectCombobox

On this page

When to useWhen not to useInstallationUsageExamplesDefaultVariantsSizesRoundedAdornmentsOption descriptions & badgesGrouped optionsStates & intentsOn colorLoading & emptyControlledSelf-previewing valuesPopup footerMobile stripPaired pickersIn a formRTLAPI ReferencePicker propsPickerOptionPickerSkeleton propsData attributesAccessibility