Select

Displays a list of options for the user to pick from—triggered by a button.

Spec · from metadata

When to use

  • Choosing a single value from a predefined list of 4+ options
  • Form fields where the user must pick from fixed options
  • Dropdown filters in toolbars or table headers

When not to use

  • Fewer than 4 options — use RadioGroup for visibility
  • Multiple selections — use Checkbox group or Combobox
  • Searchable/filterable list — use Combobox
  • Free-text input with suggestions — use Combobox or Autocomplete

Required companions

Environmental prerequisites this component needs beyond its props. Supply these or it won't work as expected — TypeScript won't catch a missing one.

CompanionKindWhy
PortalContainerProvider (existing-app only)providerSelectContent portals to <body>. In an app with its own chrome/theme, route it into a body-level .helix node at z-1300 via PortalContainerProvider (see helix-app-layouts AL1). Not needed in a greenfield app.

Variants

PropValuesDefaultDescription
size (SelectTrigger)defaultsmdefaultControls trigger height: default (h-9) or sm (h-8).

Anti-patterns

Avoid<Select onChange={handleChange}>...</Select>
Prefer<Select onValueChange={handleChange}>...</Select>

Radix Select uses onValueChange, not onChange.

Avoid<Select value={filter} onValueChange={setFilter}>
  <SelectItem value="">All states</SelectItem>
</Select>
Preferconst ALL_STATES = "all"
<Select value={filter || ALL_STATES} onValueChange={(v) => setFilter(v === ALL_STATES ? "" : v)}>
  <SelectItem value={ALL_STATES}>All states</SelectItem>
</Select>

Radix throws if a SelectItem has value="" — an empty string is reserved to clear the selection. Use a sentinel value (e.g. "all") for the no-filter option and translate it back to "" in onValueChange.

Avoid<select className="border rounded p-2">
  <option value="a">A</option>
</select>
Prefer<Select>
  <SelectTrigger>
    <SelectValue placeholder="Choose..." />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="a">A</SelectItem>
  </SelectContent>
</Select>

Use the Select component for consistent styling, keyboard navigation, and accessibility.

Avoid<FormControl>
  <Select onValueChange={field.onChange}>
    <SelectTrigger>...</SelectTrigger>
    ...
  </Select>
</FormControl>
Prefer<Select onValueChange={field.onChange}>
  <FormControl>
    <SelectTrigger>...</SelectTrigger>
  </FormControl>
  <SelectContent>...</SelectContent>
</Select>

FormControl must wrap SelectTrigger directly, not the Select root. Select is a context provider, not a DOM element.

Accessibility

  • Required ARIAaria-expanded (auto)aria-invalid (via FormControl)
  • Screen readerRadix Select provides full ARIA listbox semantics. Selected item is announced. Scroll buttons are decorative.
  • ContrastFocus items use bg-accent/text-accent-foreground. Disabled items have reduced opacity.

Token bindings

TokenCategoryUsage
bg-input-bgcolorTrigger background
border-inputcolorTrigger border
bg-popovercolorDropdown content background
text-popover-foregroundcolorDropdown text color
bg-accentcolorFocused item background
rounded-mdradiusTrigger and content border radius
shadow-mdshadowDropdown content elevation
Recent changes
  • fixed

    Primitive spec-conformance sweep

    2026-06-26

Import

import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectTrigger,
  SelectValue,
} from "@timelycare/helix-ui"

Props

interface SelectProps {
  value?: string
  defaultValue?: string
  onValueChange?: (value: string) => void
  disabled?: boolean
}

interface SelectTriggerProps {
  className?: string
  children: React.ReactNode
}

interface SelectItemProps {
  value: string
  disabled?: boolean
  className?: string
  children: React.ReactNode
}

Structure

PartTailwind
SelectTriggerh-9 px-3 py-2 gap-2 bg-input-bg border border-input rounded-md shadow-xs
Placeholdertext-sm text-muted-foreground
Selected valuetext-sm text-foreground
Chevron iconsize-4 text-muted-foreground
SelectContentbg-popover border border-border rounded-md shadow-md p-1 — uses position="popper" (default)
SelectItempl-2 pr-8 py-1.5 gap-2 rounded-sm text-sm text-popover-foreground
SelectItem hoverbg-accent text-accent-foreground
SelectLabelpx-2 py-1.5 text-xs font-semibold text-muted-foreground
Check iconsize-4 absolute right-2

States

StateTrigger Appearance
Defaultborder-input bg-input-bg placeholder visible
Focusring-2 ring-ring ring-offset-2
FilledSelected value in text-foreground
Filled (Focus)Ring + selected value
Disabledopacity-50 cursor-not-allowed

Positioning

SelectContent uses popper positioning (position="popper"), which is the default. The dropdown menu appears directly below (or above, if space is limited) the trigger, aligned to the trigger edge. It never overlaps the trigger.

Do not use position="item-aligned", which aligns the popup to the selected item and overlaps the trigger. Popper positioning keeps the trigger visible while the menu is open and provides a clearer visual relationship between the trigger and the option list.

The dropdown width matches or exceeds the trigger width via min-w-[var(--radix-select-trigger-width)].

The trigger itself is w-full, so a select lines up with the inputs above and below it in a form. For inline placements — a toolbar, a filter row, a button group — pass className="w-fit" to size it to its content, or a fixed w-[200px] when a stable width reads better than a shifting one.


Item Variants

VariantUse ForImplementation
DefaultStandard selectionText only, check on selected
With IconVisual identificationIcon (size-2.5) + text + check
CheckboxMulti-appearanceCircle indicator + text + check

Common Patterns

Basic Select

<Select>
  <SelectTrigger className="w-[200px]">
    <SelectValue placeholder="Select option" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="option1">Option 1</SelectItem>
    <SelectItem value="option2">Option 2</SelectItem>
    <SelectItem value="option3">Option 3</SelectItem>
  </SelectContent>
</Select>

With Label and Description

<div className="space-y-2">
  <Label htmlFor="framework" className="font-medium text-sm">
    Label
  </Label>
  <Select>
    <SelectTrigger id="framework" className="w-[200px]">
      <SelectValue placeholder="Placeholder" />
    </SelectTrigger>
    <SelectContent>
      <SelectItem value="next">Next.js</SelectItem>
      <SelectItem value="remix">Remix</SelectItem>
    </SelectContent>
  </Select>
  <p className="text-sm text-muted-foreground">
    This is a select description.
  </p>
</div>

With Groups

<Select>
  <SelectTrigger className="w-[200px]">
    <SelectValue placeholder="Select fruit" />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectLabel>Fruits</SelectLabel>
      <SelectItem value="apple">Apple</SelectItem>
      <SelectItem value="banana">Banana</SelectItem>
    </SelectGroup>
    <SelectGroup>
      <SelectLabel>Vegetables</SelectLabel>
      <SelectItem value="carrot">Carrot</SelectItem>
    </SelectGroup>
  </SelectContent>
</Select>

Scrollable List

<Select>
  <SelectTrigger className="w-[200px]">
    <SelectValue placeholder="Large list" />
  </SelectTrigger>
  <SelectContent className="max-h-[200px]">
    {items.map((item) => (
      <SelectItem key={item.value} value={item.value}>
        {item.label}
      </SelectItem>
    ))}
  </SelectContent>
</Select>

Accessibility

  • Built on Radix Select — handles keyboard and focus automatically
  • Keyboard: Enter/Space to open, Arrow keys to navigate, Enter to select, Escape to close
  • Screen reader: announces selected value and available options
  • Typeahead search supported
  • Requires label association via Label component

Gotchas

ProblemSolution
Placeholder not showingUse <SelectValue placeholder="..." />
Check icon misalignedItem has pr-8 to reserve space
Trigger too wide in a toolbar or button groupThe trigger fills its container by default, which suits form fields. Pass className="w-fit" for inline and compact placements, or w-[200px] for a fixed width
Scroll not showingAdd max-h-[200px] to SelectContent

See Also


Last updated: February 27, 2026