Skip to content
Ndop UI

Select primitive

Headless Select: the behavior, keyboard support and ARIA of the WAI-ARIA Select-only combobox pattern, with no styles.

Import

Primitives ship in the @ndop/primitives package and have no styles. Their state is on data attributes for you to style. The styled Select is built on this one.

import {
  NdSelect,
  NdSelectGroup,
  NdSelectItemText,
  NdSelectLabel,
  NdSelectSeparator,
  NdSelectContent,
  NdSelectItem,
  NdSelectItemIndicator,
  NdSelectTrigger,
  NdSelectValue,
} from '@ndop/primitives/select';

Accessibility

Implements the Select-only combobox pattern of the WAI-ARIA Authoring Practices.

Keyboard

Keys the component handles.

Key Action
EnterorSpaceOn the trigger, opens the listbox; on an option, selects it, closes and focuses the trigger.
ArrowDownorArrowUpOn the trigger, opens the listbox; in it, moves to the next or previous enabled option.
HomeorEndIn the listbox, moves to the first or last enabled option.
aTypeahead: on the closed trigger, selects the next option starting with the typed letters; in the listbox, moves to it.
EscapeCloses the listbox and focuses the trigger.
TabCloses the listbox and moves focus on from the trigger.

Focus

  • Opening focuses the selected option, or the first enabled one.
  • Selecting an option, Escape and Tab close the listbox; the first two return focus to the trigger. A click outside closes it where focus is.
  • Focus leaving the trigger and listbox marks a bound field touched.

Right-to-left

start and end alignments are logical and swap in right-to-left layouts. The listbox is vertical, so its arrow keys don't change.

API reference

NdSelect

[ndSelect]from @ndop/primitives/select

Root of a select: the WAI-ARIA select-only combobox pattern, a button that shows its value and opens a listbox of options. The listbox composes the CDK's CdkListbox and is written in place; opening moves it into a CDK overlay below the trigger. Put it on an ng-container, and bind forms on the NdSelectTrigger. The listbox is never shown on the server; an initial open state takes effect after hydration.

NdSelect binding Type Default
[defaultValue]
Initial value when it isn't bound.
string | nullnull
[defaultOpen]
Initial open state when open isn't bound, applied after hydration.
booleanfalse
[disabled]
Disables the trigger. A disabled bound field does too.
booleanfalse
[required]
Marks the select required: aria-required="true" on the trigger.
booleanfalse
[invalid]
Marks the select invalid when no forms library is bound; with one, the field's state decides.
booleanfalse
[(value)]
The selected option's value, or null when none is. Two-way bindable with [(value)].
string | nullnull
[(open)]
Whether the listbox is open. Two-way bindable with [(open)].
booleanfalse

NdSelectGroup

[ndSelectGroup]role groupfrom @ndop/primitives/select

Groups options (role="group"). Name it with aria-labelledby pointing at a NdSelectLabel.

NdSelectItemText

[ndSelectItemText]from @ndop/primitives/select

The text of an option, which the trigger shows while it's selected and typeahead matches. Without one, the option's own text is used.

NdSelectLabel

[ndSelectLabel]from @ndop/primitives/select

A heading for a group of options, as plain text; point the group's aria-labelledby at it.

NdSelectSeparator

[ndSelectSeparator]from @ndop/primitives/select

A decorative line between groups of options, hidden from assistive technology: a listbox may contain only options and groups.

Manages aria-hidden.

NdSelectContent

[ndSelectContent]role listboxfrom @ndop/primitives/select

The listbox of options (role="listbox"), composing CdkListbox. Write it after the trigger: it has the hidden attribute while closed, and opening moves it into an overlay below the trigger. It takes its name from the trigger's labels, or its own aria-label.

Manages aria-labelledby, aria-label, aria-orientation, id, hidden.

NdSelectContent binding Type Default
[side]
Side of the trigger it opens on, before flipping.
'start' | 'end' | 'bottom' | 'top''bottom'
[align]
Alignment along that side.
'start' | 'end' | 'center''start'
[sideOffset]
Gap between the trigger and the listbox, in pixels.
number4
Data attribute Values
data-state —
data-side —
data-align —

NdSelectItem

[ndSelectItem]role optionfrom @ndop/primitives/select

One option (role="option"), composing CdkOption. Clicking it, or Enter or Space on it, selects it and closes the listbox.

Manages aria-selected, aria-disabled, id.

NdSelectItem binding Type Default
[value]required
The value the select takes when it's chosen.
string—
[disabled]
Makes it unselectable; arrow keys skip it.
booleanfalse
[label]
Its text, when it differs from what it renders.
string | nullnull
Data attribute Values
data-state —
data-highlighted —
data-disabled —

NdSelectItemIndicator

[ndSelectItemIndicator]from @ndop/primitives/select

The check of an option. It has the hidden attribute unless its option is selected.

Manages hidden.

Data attribute Values
data-state —

NdSelectTrigger

button[ndSelectTrigger]role comboboxfrom @ndop/primitives/select

The button that shows the select's value and opens its listbox (role="combobox"), on a native button. Name it with a label[ndLabel] whose for is its id, or with aria-label. The select owns its value, so this is a ControlValueAccessor: bind Signal Forms ([formField]), reactive or template-driven forms here (ADR-0004, amendments 1 and 4).

Manages aria-haspopup, aria-expanded, aria-controls, aria-required, aria-invalid, aria-describedby.

Data attribute Values
data-state —
data-placeholder —
data-disabled —
data-invalid —

NdSelectValue

[ndSelectValue]from @ndop/primitives/select

The selected option's text, or placeholder while none is selected. Put it inside the trigger.

NdSelectValue binding Type Default
[placeholder]
What it shows while no option is selected.
string''
Data attribute Values
data-placeholder —