Skip to content
Ndop UI

Select

A button that shows a value and opens a list of options to pick from.

import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { Label } from '@/ui/label/label';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectItemIndicator,
  SelectItemText,
  SelectTrigger,
  SelectValue,
} from '@/ui/select/select';

/**
 * A fruit picker with a placeholder. Typing a letter on the closed select
 * picks the next fruit starting with it.
 */
@Component({
  selector: 'example-select-demo',
  imports: [
    Label,
    NdIcon,
    Select,
    SelectContent,
    SelectItem,
    SelectItemIndicator,
    SelectItemText,
    SelectTrigger,
    SelectValue,
  ],
  template: `
    <div class="grid gap-3">
      <label ndLabel for="select-demo-fruit">Fruit</label>
      <ng-container ndSelect [(value)]="fruit">
        <button ndSelectTrigger id="select-demo-fruit" class="w-[180px]">
          <span ndSelectValue placeholder="Select a fruit"></span>
          <svg ndIcon="chevron-down" class="size-4 opacity-50"></svg>
        </button>
        <div ndSelectContent>
          @for (option of fruits; track option.value) {
            <div ndSelectItem [value]="option.value">
              <span ndSelectItemIndicator><svg ndIcon="check"></svg></span>
              <span ndSelectItemText>{{ option.label }}</span>
            </div>
          }
        </div>
      </ng-container>
    </div>
  `,
})
export class SelectDemoExample {
  protected readonly fruit = signal<string | null>(null);
  protected readonly fruits = [
    { value: 'apple', label: 'Apple' },
    { value: 'banana', label: 'Banana' },
    { value: 'blueberry', label: 'Blueberry' },
    { value: 'grapes', label: 'Grapes' },
    { value: 'pineapple', label: 'Pineapple' },
  ];
}

A select shows the chosen value on a button that opens a list of options below it. Typing on the closed button picks the next option starting with those letters, as on a native select; open, the arrow keys and typing move through the options and Enter picks one. The options are written in place, so the button shows the chosen option's text on the server. Bind forms on its trigger.

Installation

npx @ndop/cli add select

Usage

import { Label } from '@/ui/label/label';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectItemIndicator,
  SelectItemText,
  SelectTrigger,
  SelectValue,
} from '@/ui/select/select';
<div class="grid gap-3">
  <label ndLabel for="select-demo-fruit">Fruit</label>
  <ng-container ndSelect [(value)]="fruit">
    <button ndSelectTrigger id="select-demo-fruit" class="w-[180px]">
      <span ndSelectValue placeholder="Select a fruit"></span>
      <svg ndIcon="chevron-down" class="size-4 opacity-50"></svg>
    </button>
    <div ndSelectContent>
      @for (option of fruits; track option.value) {
        <div ndSelectItem [value]="option.value">
          <span ndSelectItemIndicator><svg ndIcon="check"></svg></span>
          <span ndSelectItemText>{{ option.label }}</span>
        </div>
      }
    </div>
  </ng-container>
</div>

Examples

Select groups

A long list of options in labelled groups.

import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectItemIndicator,
  SelectItemText,
  SelectLabel,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
} from '@/ui/select/select';

/** A long list of time zones in labelled groups, which scrolls. */
@Component({
  selector: 'example-select-groups',
  imports: [
    NdIcon,
    Select,
    SelectContent,
    SelectGroup,
    SelectItem,
    SelectItemIndicator,
    SelectItemText,
    SelectLabel,
    SelectSeparator,
    SelectTrigger,
    SelectValue,
  ],
  template: `
    <ng-container ndSelect>
      <button ndSelectTrigger aria-label="Time zone" class="w-[280px]">
        <span ndSelectValue placeholder="Select a time zone"></span>
        <svg ndIcon="chevron-down" class="size-4 opacity-50"></svg>
      </button>
      <div ndSelectContent>
        @for (region of regions; track region.name; let last = $last) {
          <div ndSelectGroup [attr.aria-labelledby]="'select-groups-' + $index">
            <div ndSelectLabel [id]="'select-groups-' + $index">
              {{ region.name }}
            </div>
            @for (zone of region.zones; track zone.value) {
              <div ndSelectItem [value]="zone.value">
                <span ndSelectItemIndicator><svg ndIcon="check"></svg></span>
                <span ndSelectItemText>{{ zone.label }}</span>
              </div>
            }
          </div>
          @if (!last) {
            <div ndSelectSeparator></div>
          }
        }
      </div>
    </ng-container>
  `,
})
export class SelectGroupsExample {
  protected readonly regions = [
    {
      name: 'North America',
      zones: [
        { value: 'est', label: 'Eastern Standard Time (EST)' },
        { value: 'cst', label: 'Central Standard Time (CST)' },
        { value: 'mst', label: 'Mountain Standard Time (MST)' },
        { value: 'pst', label: 'Pacific Standard Time (PST)' },
      ],
    },
    {
      name: 'Europe and Africa',
      zones: [
        { value: 'gmt', label: 'Greenwich Mean Time (GMT)' },
        { value: 'cet', label: 'Central European Time (CET)' },
        { value: 'wat', label: 'West Africa Time (WAT)' },
        { value: 'eat', label: 'East Africa Time (EAT)' },
      ],
    },
    {
      name: 'Asia',
      zones: [
        { value: 'ist', label: 'India Standard Time (IST)' },
        { value: 'cst-china', label: 'China Standard Time (CST)' },
        { value: 'jst', label: 'Japan Standard Time (JST)' },
      ],
    },
  ];
}

Select sizes

The default and small triggers.

import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectItemIndicator,
  SelectItemText,
  SelectTrigger,
  SelectValue,
} from '@/ui/select/select';

/** The default and small triggers. */
@Component({
  selector: 'example-select-sizes',
  imports: [
    NdIcon,
    Select,
    SelectContent,
    SelectItem,
    SelectItemIndicator,
    SelectItemText,
    SelectTrigger,
    SelectValue,
  ],
  template: `
    <div class="flex flex-wrap items-center gap-4">
      @for (size of sizes; track size) {
        <ng-container ndSelect defaultValue="week">
          <button
            ndSelectTrigger
            [size]="size"
            [attr.aria-label]="'Range, ' + size + ' size'"
            class="w-[140px]"
          >
            <span ndSelectValue></span>
            <svg ndIcon="chevron-down" class="size-4 opacity-50"></svg>
          </button>
          <div ndSelectContent>
            @for (range of ranges; track range.value) {
              <div ndSelectItem [value]="range.value">
                <span ndSelectItemIndicator><svg ndIcon="check"></svg></span>
                <span ndSelectItemText>{{ range.label }}</span>
              </div>
            }
          </div>
        </ng-container>
      }
    </div>
  `,
})
export class SelectSizesExample {
  protected readonly sizes = ['default', 'sm'] as const;
  protected readonly ranges = [
    { value: 'day', label: 'Today' },
    { value: 'week', label: 'This week' },
    { value: 'month', label: 'This month' },
  ];
}

Select disabled

A disabled select, and one with a disabled option.

import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectItemIndicator,
  SelectItemText,
  SelectTrigger,
  SelectValue,
} from '@/ui/select/select';

/**
 * A disabled select, and one with a disabled option, which the arrow keys
 * skip.
 */
@Component({
  selector: 'example-select-disabled',
  imports: [
    NdIcon,
    Select,
    SelectContent,
    SelectItem,
    SelectItemIndicator,
    SelectItemText,
    SelectTrigger,
    SelectValue,
  ],
  template: `
    <div class="flex flex-wrap items-center gap-4">
      <ng-container ndSelect disabled>
        <button ndSelectTrigger aria-label="Plan" class="w-[180px]">
          <span ndSelectValue placeholder="Select a plan"></span>
          <svg ndIcon="chevron-down" class="size-4 opacity-50"></svg>
        </button>
        <div ndSelectContent>
          <div ndSelectItem value="free">
            <span ndSelectItemText>Free</span>
          </div>
        </div>
      </ng-container>
      <ng-container ndSelect>
        <button ndSelectTrigger aria-label="Seats" class="w-[180px]">
          <span ndSelectValue placeholder="Select seats"></span>
          <svg ndIcon="chevron-down" class="size-4 opacity-50"></svg>
        </button>
        <div ndSelectContent>
          <div ndSelectItem value="1">
            <span ndSelectItemIndicator><svg ndIcon="check"></svg></span>
            <span ndSelectItemText>1 seat</span>
          </div>
          <div ndSelectItem value="5">
            <span ndSelectItemIndicator><svg ndIcon="check"></svg></span>
            <span ndSelectItemText>5 seats</span>
          </div>
          <div ndSelectItem value="unlimited" disabled>
            <span ndSelectItemIndicator><svg ndIcon="check"></svg></span>
            <span ndSelectItemText>Unlimited (Enterprise)</span>
          </div>
        </div>
      </ng-container>
    </div>
  `,
})
export class SelectDisabledExample {}

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

Select

[ndSelect]

A select (@ndop/primitives/select): a button that shows its value and opens a list of options below it. Put it on an ng-container, and bind forms on its trigger.

Select 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

SelectTrigger

button[ndSelectTrigger]role combobox

The button that shows the value and opens the options. size is default or sm. Put the chevron icon in it, last.

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

SelectTrigger binding Type Default
[size]
Height: default or sm.
'default' | 'sm''default'
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotselect-trigger
data-size —
data-state —
data-placeholder —
data-disabled —

SelectValue

[ndSelectValue]

The selected option's text, or placeholder while none is selected.

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

SelectContent

[ndSelectContent]role listbox

The list of options, written after the trigger; it opens below the trigger, at least as wide.

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

SelectContent binding Type Default
[class]
Extra classes, merged last so they win.
string''
[side]
Side of the trigger it opens on, before flipping.
'bottom' | 'top' | 'start' | 'end''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-slotselect-content
data-state —
data-side —

SelectLabel

[ndSelectLabel]

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

SelectLabel binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotselect-label

SelectGroup

[ndSelectGroup]role group

Groups options; name it with aria-labelledby.

Data attribute Values
data-slotselect-group

SelectItem

[ndSelectItem]role option

One option. Put its SelectItemIndicator first and its SelectItemText after.

Manages aria-selected, aria-disabled, id.

SelectItem binding Type Default
[class]
Extra classes, merged last so they win.
string''
[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-slotselect-item
data-state —
data-highlighted —
data-disabled —

SelectItemIndicator

[ndSelectItemIndicator]

The check of the selected option, at its end.

Manages hidden.

SelectItemIndicator binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotselect-item-indicator
data-state —

SelectItemText

[ndSelectItemText]

The text of an option, which the trigger shows while it's selected.

Data attribute Values
data-slotselect-item-text

SelectSeparator

[ndSelectSeparator]

A line between groups of options.

Manages aria-hidden.

SelectSeparator binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotselect-separator

Agent notes

Do

  • Name the trigger with a label, as any form field.
  • Group long lists, with labels.
  • Use size="sm" in dense layouts.

Don't

  • Don't use it for more than a few dozen options, or when people know what they want to type; use a Combobox.
  • Don't use it for actions; use Dropdown Menu.