Spec version v0.2.0

Select

Purpose

A Select is a form control that allows a user to choose one value from a predefined list of options. Use it when the list of choices is fixed and known ahead of time, the user can only pick one value at a time, and the list contains more items than a Radio Group can reasonably display (generally more than five options). Do NOT use Select when you need multi-selection — use a Multi-Select or Checkbox Group instead. Do NOT use Select when the user needs to type a search query to filter options — use a Combobox instead. Do NOT use Select for binary on/off choices — use a Toggle or Switch instead.

Like Input, a Select is almost always wrapped in a Field component that provides the label, description, and error message.

Anatomy

A Select is composed of two primary structural parts: the trigger and the dropdown panel.

Trigger (always visible):

  • Label (managed by Field): short noun phrase above (vertical) or beside (horizontal) the trigger.
  • Trigger button: the interactive element the user clicks to open the dropdown. Displays either a placeholder string (when no value is selected) or the currently selected option's label.
  • Selected value display: the text of the chosen option. If the option includes an icon or decorative element, that element may appear here too.
  • Chevron icon: a downward-pointing chevron on the right edge of the trigger, indicating the control opens a list. When the dropdown is open, the chevron rotates 180 degrees to point upward.

Dropdown panel (appears on open):

  • Scroll up button (appears when list overflows upward): a small centered chevron button users can hover/click to scroll up.
  • Viewport: the scrollable list of options.
  • Option group label (optional): a non-interactive label separating named categories of options within the list.
  • Option item: a single selectable row. Contains the option's text label and optionally a leading icon. When selected, a checkmark icon appears on the right.
  • Separator (optional): a horizontal rule separating logical groups when group labels are not used.
  • Scroll down button (appears when list overflows downward): same as scroll up, mirrored.

Variants

The Select trigger inherits the Input-level styling approach. There are two sizes:

default (40px trigger height)

Use in standard form layouts. This matches the medium Input size and should be used whenever the Select sits alongside other form controls.

sm (32px trigger height)

Use in compact toolbars, filter controls, and inline density contexts. Match the size to adjacent controls — do not mix default and sm selects in the same row of controls.

The dropdown panel itself has no size variants. Its width matches the trigger width (minimum); it may expand wider to accommodate long option labels.

States

Default (closed, no value): Trigger shows placeholder text in --force-color-text-disabled. Chevron points down.

Default (closed, with value): Trigger shows selected option label in --force-color-text-primary. Chevron points down.

Hover (trigger): Trigger border darkens to --force-color-border-strong. Background transitions to --force-color-bg-interactive-hover. Transition at --force-duration-fast.

Focus (keyboard, trigger closed): Trigger border changes to --force-color-border-focus. Focus ring --force-shadow-focus-ring is applied. Chevron points down. The dropdown does NOT open automatically on focus; it opens on Space, Enter, or click.

Open: Trigger border is --force-color-border-focus. Chevron rotates 180 degrees. The dropdown panel appears below the trigger (default) or above if there is insufficient space below. The panel is rendered in a portal at z-index: --force-z-index-dropdown (100). Opening and closing animate with a 150ms fade + scale (--force-duration-fast).

Option hover: The hovered option's background transitions to --force-color-bg-interactive-hover. Text color is --force-color-text-primary.

Option focus (keyboard navigation): Same visual treatment as hover. The focused option receives focus treatment, not the trigger.

Option selected: The selected option shows a checkmark icon on the right. When the dropdown is reopened, the selected option has --force-color-bg-interactive-active background and --force-color-text-interactive-active text.

Disabled (trigger): The trigger is non-interactive. Apply opacity: --force-opacity-disabled (0.5) to the entire Field. Cursor is not-allowed. The dropdown cannot be opened.

Disabled (individual option): A disabled option is visible but non-interactive. Opacity 0.5, cursor not-allowed. Disabled options should be avoided when possible — if an option is never valid, remove it from the list rather than disabling it in-place.

Error: Trigger border changes to --force-color-border-error. When focused in error state, focus ring is --force-shadow-focus-ring-error. The Field Error message appears below the trigger. aria-invalid="true" is set on the trigger.

Behavior

  • Opening: The dropdown opens on click, Space, or Enter while the trigger is focused. Arrow Up / Arrow Down also opens the dropdown and moves focus to the first/last option respectively.
  • Closing: The dropdown closes when: the user selects an option, the user presses Escape, the user clicks outside the dropdown, or focus moves outside the composite.
  • Selection: Clicking an option or pressing Enter/Space on a focused option selects it, updates the trigger display, and closes the dropdown.
  • Keyboard navigation within dropdown: Arrow Down moves focus to the next option. Arrow Up moves focus to the previous option. Home moves to the first option. End moves to the last option. Type-ahead is supported: typing a character moves focus to the first option matching that character.
  • Scroll behavior: The dropdown panel has a maximum height (typically 200–300px). When the list exceeds this height, it becomes scrollable. The scroll-up and scroll-down buttons appear when the list overflows in the respective direction.
  • Positioning: The dropdown opens downward by default. If there is insufficient space below the trigger, it opens upward. Left-aligns with the trigger. The panel's width matches the trigger's width at minimum and may expand to fit option content.
  • Portal rendering: The dropdown content is rendered in a portal (appended to <body>) to prevent overflow clipping from ancestor overflow: hidden containers.

Accessibility

  • The trigger element is a <button> with role="combobox", aria-haspopup="listbox", and aria-expanded (true when open, false when closed).
  • The dropdown panel has role="listbox". Each option has role="option". The currently selected option has aria-selected="true"; all others have aria-selected="false".
  • The trigger's accessible name comes from the associated <label> via aria-labelledby or a direct aria-label.
  • The active (keyboard-focused) option within the list is tracked via aria-activedescendant on the trigger, pointing to the focused option's id.
  • When the dropdown closes after selection, focus returns to the trigger automatically.
  • The scroll buttons (scroll up / scroll down) are presentational aids; keyboard users do not interact with them directly — keyboard arrow navigation handles scrolling.
  • If aria-invalid="true" is set on the trigger, associate the error message element via aria-describedby on the trigger.
  • Group labels (non-interactive) must have role="group" with aria-label applied to a wrapping element, or use role="presentation" on the label row so screen readers skip over it as a focusable item.

Composition

  • A Select is almost always wrapped in a Field (label, description, error).
  • Multiple Selects in a form use FieldGroup for consistent vertical spacing.
  • In a ControlsBar (filter row above a data table), a Select is paired with Input search, date pickers, and action Buttons in a horizontal flex row with --force-spacing-4 (16px) gap.
  • Select can appear inside a multi-step Dialog (Wizard) as a step's primary form control.
  • Do NOT use Select inside a Popover as the primary control — if the use case is inline filtering, use a Combobox or a dedicated filter panel.
  • Do NOT combine Select with Input in a single composite trigger (e.g., "unit selector" appended to a number input) unless you are explicitly building a custom addon input pattern. Keep the two controls as separate Fields.

Guidance

Choose Select over other controls:

  • Use Select (not Radio Group) when there are more than five mutually exclusive options, or when the list may grow dynamically.
  • Use Select (not Combobox) when the user cannot type a value — they can only pick from the exact list provided.
  • Use Select (not Multi-Select) when only one value may be chosen.

Naming options:

  • Keep option labels concise: two to four words. Avoid starting every option with the same word (e.g., "No filter", "No value", "No selection" — instead, use a placeholder for the empty state and positive labels for options).
  • Order options meaningfully: alphabetically, by frequency of use, or by logical progression. Do not order arbitrarily.
  • Use a blank / placeholder option (e.g., "Select a region…") as the initial state when no default value exists. Do NOT pre-select a value silently unless the system can reasonably determine the correct default.

Implementation requirement — explicit visual properties:

The Select trigger is a styled <button>, but the same rule applies as for all form elements: explicitly set background, color, and border using Force UI tokens on both the trigger and the dropdown panel. Do NOT rely on browser defaults. Browsers and host pages may apply dark-mode styles that override unset properties. See design principle P8 and the Token Usage section below.

Common mistakes to avoid:

  • Do NOT leave background, color, or border unset on the Select trigger or dropdown — they will inherit browser/host dark-mode defaults.
  • Do NOT use a Select with only two options. A two-option choice is a binary decision — use a Toggle, Switch, or Radio Group.
  • Do NOT put more than 50 options in a plain Select — switch to a Combobox with search at that scale.
  • Do NOT disable the placeholder option once a value is chosen to "lock in" the selection. Allow users to reset to no-selection by providing a "clear" mechanism or making the placeholder selectable.

Token Usage

Trigger (default state, no value)

  • Background: --force-color-bg-surface
  • Border: 1px solid --force-color-border-default
  • Text (placeholder): --force-color-text-disabled
  • Chevron icon color: --force-color-text-disabled
  • Border radius: --force-radius-input (6px)

Trigger (default state, has value)

  • Text: --force-color-text-primary
  • Chevron icon color: --force-color-text-disabled

Trigger hover

  • Background: --force-color-bg-interactive-hover
  • Border: --force-color-border-strong

Trigger focus

  • Border: --force-color-border-focus
  • Focus ring: --force-shadow-focus-ring

Trigger error

  • Border: --force-color-border-error
  • Error focus ring: --force-shadow-focus-ring-error

Trigger disabled

  • Background: --force-color-bg-muted
  • Opacity (entire Field): --force-opacity-disabled (0.5)

Dropdown panel

  • Background: --force-color-bg-surface
  • Border: 1px solid --force-color-border-default
  • Border radius: --force-radius-card (8px)
  • Shadow: --force-shadow-sm
  • Z-index: --force-z-index-dropdown (100)

Option item (default)

  • Text: --force-color-text-primary
  • Background: transparent

Option item hover / keyboard focus

  • Background: --force-color-bg-interactive-hover
  • Text: --force-color-text-primary

Option item selected

  • Background: --force-color-bg-interactive-active
  • Text: --force-color-text-interactive-active
  • Checkmark icon: --force-color-text-interactive-active

Option group label

  • Text: --force-color-text-tertiary
  • Font size: --force-font-size-xs (12px)
  • Font weight: --force-font-weight-medium (500)

Separator

  • Color: --force-color-border-default

Label / Field

  • Same tokens as Input label and helper text

Transition

  • Duration: --force-duration-fast (150ms)
  • Easing: --force-easing-standard