Skip to content

Select

Reach for Select when a value comes from a short, fixed set of options and the user picks exactly one: a time zone, a plan, a deploy region. It renders a button that opens a listbox, keeps the chosen label in the trigger, and submits through a hidden input when it sits in a form. It has typeahead but no filtering — for lists long enough to need a search field, use Combobox instead.

select/basic.tsx

Pass items to <Select.Root> so <Select.Value> renders the option's label instead of the raw value — without it the trigger above would read Europe/London. The popup opens over the trigger with the selected row's text sitting on the trigger's text; that is alignItemWithTrigger, on by default — see Placement for the trade-off and how to turn it off.

Import

import { Select } from "@forte-ui/react";

Examples

Labelling

<Select.Label> is the labelling pattern to reach for. It renders a <div> wired to the trigger with aria-labelledby, so clicking it focuses the trigger without opening the popup.

select/with-label.tsx
Interface language

Grouped options

<Select.Group> renders role="group" and takes its accessible name from the <Select.GroupLabel> inside it, so screen readers announce the heading along with its members. The group label is indented past the check-mark column so its text lines up with the item text.

select/grouped.tsx
Deploy region

Placement

By default the popup overlaps the trigger, with the selected row's text landing on the trigger's own text — alignItemWithTrigger, which is what a native <select> does on macOS. Pass alignItemWithTrigger={false} to drop the popup below the trigger instead, the way a Menu opens.

select/menu-placement.tsx
Aligned to selection
Menu placement

Aligned placement puts the current value under the pointer at the moment the popup opens, so re-picking the same option is a click without a journey. It costs the trigger: the popup covers it, and it needs room above and below, which is why Base UI drops back to side placement for touch input and when the viewport is too tight — so it is the mode users see least consistently. Menu placement never covers the trigger and behaves identically everywhere, which is what you want for a select sitting in a dense toolbar or next to a value the user is comparing against.

The two modes animate differently, and that is not a knob. Aligned mode has no side to rise from, so it unfurls out of the selected row with --transform-origin pinned there; side placement unfolds away from the trigger edge and adds a short slide when it lands on an inline side. side, align, sideOffset and alignOffset apply only to the second — Base UI ignores all four while the popup is aligned.

Sizes

size sets the trigger's height, inline padding and font size — nothing else. The popup is unaffected: items keep their own type scale and their --forte-target-min (24px) minimum row height whatever size the trigger is.

select/sizes.tsx
Small
Medium
Large

Variants

variant changes only how much weight the trigger carries: outline reads as a form control, soft as a filled field, ghost as an inline affordance.

select/variants.tsx
Outline
Soft
Ghost

Disabled items

A disabled item stays in the list and stays announced, so the option remains discoverable — it just cannot be chosen. Typeahead skips it, matching what a native <select> does. Say in the item's own text why it is unavailable; greying alone tells the user nothing.

select/disabled-items.tsx
Export format

Accessibility

Keyboard interactions
KeyBehaviour
EnterWith the trigger focused, opens the popup and highlights the selected item. With the popup open, selects the highlighted item and closes.
SpaceSame as Enter, except while typeahead is mid-word, where it types a space instead of selecting.
Arrow DownOpens the popup from the trigger; moves the highlight down one item once open.
Arrow UpOpens the popup from the trigger; moves the highlight up one item once open.
HomeHighlights the first item.
EndHighlights the last item.
A–ZTypeahead. With the popup open, highlights the first item whose text matches what you type; with it closed, it changes the value directly, like a native select. Disabled items are skipped and the buffer resets 750ms after the last keystroke.
EscCloses the popup without changing the value and returns focus to the trigger.

The highlight does not wrap: Arrow Down on the last item and Arrow Up on the first stay put rather than cycling.

data-highlighted is the focus analogue for items. Base UI moves real DOM focus between options with a roving tabindex, so the highlighted item is the focused element, and pointer hover sets the same attribute (highlightItemOnHover on <Select.Root>, on by default — set it to false if you need CSS :hover to be distinguishable from the focused state). Style rows off [data-highlighted] rather than :hover, or keyboard users get no highlight at all.

The scroll arrows at the top and bottom of a long popup are a pointer convenience only. Base UI does not render them for touch input, marks them aria-hidden, and hides them once the list cannot scroll further — the list itself scrolls by wheel, drag, keyboard and typeahead, so no item is ever reachable only through an arrow. Turning them off with scrollArrows={false} removes nothing but the affordance.

Theming

Every --forte-select-* property below is declared on the surface that owns it, and an element's own declaration beats an inherited value — so setting one on :root, on a theme scope or on any ancestor has no effect. The popup is portalled to <body> as well, so an ancestor of the trigger is not even in its tree. Set each property on the part that declares it, through that part's className (an unlayered rule beats the library's @layer forte.components, whatever its specificity) or an inline style:

  • Select.Label declares the four label-* properties.
  • Select.Trigger declares every trigger-* property.
  • Select.Popup declares the popup-* properties plus --forte-select-item-px, --forte-select-item-gap and --forte-select-item-indicator-size — the layout knobs the list, the items and the group labels read but never redeclare, so the popup is the right place to set them.
  • positionerClassName reaches --forte-select-z-index and --forte-select-positioner-min-height, and nothing else: those two are the only properties declared on the positioner.
  • Select.Item declares --forte-select-item-fg, --forte-select-item-bg-highlighted, --forte-select-item-fg-highlighted, --forte-select-item-fg-selected and --forte-select-item-font-size.
  • --forte-select-backdrop-z-index, --forte-select-list-padding-y and --forte-select-scroll-arrow-size are declared on the backdrop, the list and the scroll arrows — elements Select.Popup renders internally and exposes no class name for, so they cannot be retargeted from outside.

The global tokens these resolve to — --forte-color-*, --forte-control-*, --forte-radius-*, --forte-space-* — are the exception: the component only reads those and never redeclares them, so re-pointing them on :root or on a theme scope does re-skin every select.

Theming tokens for Select
PropertyControlsDefault
--forte-select-label-fgLabel text colourvar(--forte-color-foreground)
--forte-select-label-font-sizeLabel font sizevar(--forte-font-size-2)
--forte-select-label-font-weightLabel font weightvar(--forte-font-weight-medium)
--forte-select-label-gapSpace between label and triggervar(--forte-space-1)
--forte-select-trigger-heightTrigger height (reset per size)var(--forte-control-h-md)
--forte-select-trigger-padding-xTrigger inline padding (reset per size)var(--forte-control-px-md)
--forte-select-trigger-font-sizeTrigger font size (reset per size)var(--forte-font-size-2)
--forte-select-trigger-gapGap between value and iconvar(--forte-control-gap)
--forte-select-trigger-radiusTrigger corner radiusvar(--forte-radius-control)
--forte-select-trigger-border-widthTrigger border width1px
--forte-select-trigger-bgTrigger background (reset per variant)var(--forte-color-background)
--forte-select-trigger-bg-hoverTrigger background on hovervar(--forte-color-panel-hover)
--forte-select-trigger-bg-activeTrigger background while pressed or openvar(--forte-color-panel-active)
--forte-select-trigger-fgTrigger text colourvar(--forte-color-foreground)
--forte-select-trigger-border-colorTrigger border colour (reset per variant)var(--forte-color-border)
--forte-select-backdrop-z-indexStacking of the optional backdrop layer49
--forte-select-z-indexStacking of the positioner50
--forte-select-positioner-min-heightHow tall the popup must be able to get before alignItemWithTrigger falls back to side placement — smaller falls back less oftencalc(var(--forte-control-h-md) * 2.5)
--forte-select-popup-bgPopup surface colourvar(--forte-color-overlay)
--forte-select-popup-fgPopup text colourvar(--forte-color-foreground)
--forte-select-popup-radiusPopup corner radiusvar(--forte-radius-surface)
--forte-select-popup-shadowPopup shadowvar(--forte-shadow-4)
--forte-select-popup-border-widthWidth of the hairline that separates the popup from the page1px
--forte-select-popup-border-colorColour of that hairlinevar(--forte-color-border)
--forte-select-popup-overhangExtra width the popup claims past the trigger in alignItemWithTrigger modevar(--forte-space-5)
--forte-select-item-pxInline padding shared by items and group labelsvar(--forte-space-3)
--forte-select-item-gapGap between the indicator column and the item textvar(--forte-control-gap)
--forte-select-item-indicator-sizeWidth of the check-mark columnvar(--forte-space-4)
--forte-select-list-padding-yBlock padding inside the scrolling listvar(--forte-space-1)
--forte-select-scroll-arrow-sizeHeight of a scroll arrowvar(--forte-space-5)
--forte-select-item-fgItem text colourvar(--forte-color-foreground)
--forte-select-item-bg-highlightedBackground of the highlighted itemvar(--forte-color-primary-soft)
--forte-select-item-fg-highlightedText colour of the highlighted itemvar(--forte-color-primary-text)
--forte-select-item-fg-selectedText colour of the selected itemvar(--forte-color-primary-text)
--forte-select-item-font-sizeItem font sizevar(--forte-font-size-2)

API reference

Select.Root

Groups every part and owns the value and open state. It renders no DOM element, so it takes neither className nor ref, and it forwards Base UI's own Select.Root props unchanged — including value, defaultValue, onValueChange, items, multiple, name, disabled, readOnly, required, modal and highlightItemOnHover. Both generics are preserved, so <Select.Root<Theme>> types onValueChange and defaultValue for you.

Select.Label

Props for SelectLabel
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.

Select.Trigger

Props for SelectTrigger
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
fullWidthbooleanfalseStretch the trigger to fill the width of its container.
sizeSelectSizemdSize of the trigger. Actual dimensions also follow the ambient `data-forte-density` setting.
variantSelectVariantoutlineHow much visual weight the trigger carries. `outline` reads as a form control, `soft` as a filled field, `ghost` as an inline affordance.

Select.Value

Props for SelectValue
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.

Select.Icon

Props for SelectIcon
PropTypeDefaultDescription
childrenReactNode<CaretUpDownIcon />Icon to render. Defaults to a caret pair, which stays correct whichever side the popup ends up on — including `alignItemWithTrigger` mode, where it opens over the trigger and has no side at all.
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.

Select.Popup

Renders the whole floating half of the anatomy — portal, optional backdrop, positioner, popup, scroll arrows and the scrolling list — as one part.

Props for SelectPopup
PropTypeDefaultDescription
alignAligncenterHow the popup aligns along the chosen side. Ignored while `alignItemWithTrigger` is in effect.
alignItemWithTriggerbooleantrueWhether the popup overlaps the trigger so the selected item's text lines up with the trigger's value text. While this is active `data-side` is `"none"` on both the positioner and the popup, `side`/`align`/`sideOffset` are ignored, and the enter/exit transition is suppressed. Base UI also disables it automatically for touch input and when the viewport is too tight, in which case ordinary side placement takes over.
alignOffsetnumber | OffsetFunction0Extra offset in pixels along the alignment axis. Ignored while `alignItemWithTrigger` is in effect.
backdropbooleanfalseRender a dimming layer behind the popup. Off by default — a select is a menu, not a dialog — but useful on small screens where the popup covers most of the page.
childrenReactNodeThe items, separators and groups to render. They are placed inside `<Select.List>`, which is the element that actually scrolls.
classNamestringAdditional class name(s) for the popup surface. Applied after the internal styles so consumer utilities win without needing `!important`.
collisionPaddingPadding5Space to keep between the popup and the edge of its collision boundary.
containerHTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | nullRender the popup into a different container instead of `<body>`.
finalFocusboolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)the triggerWhere focus goes when the popup closes. There is no `initialFocus` counterpart on a select popup — Base UI always moves focus to the highlighted item on open.
positionerClassNamestringAdditional class name(s) for the positioner — the absolutely positioned wrapper around the popup. Use this for `z-index` or a `min-height` that tunes how often `alignItemWithTrigger` falls back.
scrollArrowsbooleantrueRender the hover-to-scroll arrows at the top and bottom of the list. They are a pointer convenience only: Base UI does not render them for touch input, and the list scrolls by wheel, drag, keyboard and typeahead regardless, so no item is ever reachable only through them.
sideSidebottomWhich side of the trigger the popup opens on. Ignored while `alignItemWithTrigger` is in effect.
sideOffsetnumber | OffsetFunction4Gap in pixels between the trigger and the popup. Ignored while `alignItemWithTrigger` is in effect.

Select.Item

Props for SelectItem
PropTypeDefaultDescription
childrenReactNodeThe item's label. Rendered inside `<Select.ItemText>`, which is what `<Select.Value>` mirrors in the trigger and what typeahead matches on.
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
disabledbooleanfalseWhether the item ignores user interaction. Disabled items stay in the list and stay announced; typeahead skips them.
indicatorReactNode<CheckIcon />What marks the item as selected. Rendered inside `<Select.ItemIndicator>`, which only mounts while the item is selected.
labelstringPlain-text label used for typeahead when `children` is not a string.
valueunknownnullThe value this item selects. `null` is a legitimate value and is how you offer a "clear the selection" row inside the list.

Select.Group

Props for SelectGroup
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.

Select.GroupLabel

Props for SelectGroupLabel
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.

Select.Separator

Props for SelectSeparator
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
orientationOrientationhorizontalOrientation of the rule. A select list is vertical, so the separator between two groups runs horizontally.