Skip to content
Ndop UI

Combobox

A text field that filters a list of options as the user types.

import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Combobox,
  ComboboxContent,
  ComboboxControl,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxItemIndicator,
  ComboboxList,
  ComboboxTrigger,
} from '@/ui/combobox/combobox';
import { Label } from '@/ui/label/label';

/**
 * A framework picker that filters as you type. Focus stays in the field;
 * the arrow keys move through the options and Enter picks one.
 */
@Component({
  selector: 'example-combobox-demo',
  imports: [
    Combobox,
    ComboboxContent,
    ComboboxControl,
    ComboboxEmpty,
    ComboboxInput,
    ComboboxItem,
    ComboboxItemIndicator,
    ComboboxList,
    ComboboxTrigger,
    Label,
    NdIcon,
  ],
  template: `
    <div class="grid w-[240px] gap-3">
      <label ndLabel for="combobox-demo-framework">Framework</label>
      <ng-container ndCombobox [(value)]="framework">
        <div ndComboboxControl>
          <input
            ndComboboxInput
            id="combobox-demo-framework"
            placeholder="Select a framework"
          />
          <button ndComboboxTrigger><svg ndIcon="chevron-down"></svg></button>
        </div>
        <div ndComboboxContent>
          <div ndComboboxList>
            <div ndComboboxEmpty>No framework found.</div>
            @for (option of frameworks; track option.value) {
              <div ndComboboxItem [value]="option.value">
                {{ option.label }}
                <span ndComboboxItemIndicator><svg ndIcon="check"></svg></span>
              </div>
            }
          </div>
        </div>
      </ng-container>
    </div>
  `,
})
export class ComboboxDemoExample {
  protected readonly framework = signal<string | null>(null);
  protected readonly frameworks = [
    { value: 'angular', label: 'Angular' },
    { value: 'analog', label: 'Analog' },
    { value: 'astro', label: 'Astro' },
    { value: 'nuxt', label: 'Nuxt' },
    { value: 'remix', label: 'Remix' },
    { value: 'sveltekit', label: 'SvelteKit' },
  ];
}

A combobox is a text field that filters a list of options below it as the user types, for picking from many options, such as a country or a framework. Focus stays in the field while the arrow keys move through the options and Enter picks one. With multiple, the chosen options show as chips beside the field, and Backspace removes the last one. Bind forms on its field.

Installation

npx @ndop/cli add combobox

Usage

import {
  Combobox,
  ComboboxContent,
  ComboboxControl,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxItemIndicator,
  ComboboxList,
  ComboboxTrigger,
} from '@/ui/combobox/combobox';
import { Label } from '@/ui/label/label';
<div class="grid w-[240px] gap-3">
  <label ndLabel for="combobox-demo-framework">Framework</label>
  <ng-container ndCombobox [(value)]="framework">
    <div ndComboboxControl>
      <input
        ndComboboxInput
        id="combobox-demo-framework"
        placeholder="Select a framework"
      />
      <button ndComboboxTrigger><svg ndIcon="chevron-down"></svg></button>
    </div>
    <div ndComboboxContent>
      <div ndComboboxList>
        <div ndComboboxEmpty>No framework found.</div>
        @for (option of frameworks; track option.value) {
          <div ndComboboxItem [value]="option.value">
            {{ option.label }}
            <span ndComboboxItemIndicator><svg ndIcon="check"></svg></span>
          </div>
        }
      </div>
    </div>
  </ng-container>
</div>

Examples

Combobox clear

A combobox with a button that clears it.

import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Combobox,
  ComboboxClear,
  ComboboxContent,
  ComboboxControl,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxItemIndicator,
  ComboboxList,
  ComboboxTrigger,
} from '@/ui/combobox/combobox';

/**
 * A combobox with a button that clears it, shown in place of the chevron
 * once there is something to clear. Escape clears it from the keyboard.
 */
@Component({
  selector: 'example-combobox-clear',
  imports: [
    Combobox,
    ComboboxClear,
    ComboboxContent,
    ComboboxControl,
    ComboboxEmpty,
    ComboboxInput,
    ComboboxItem,
    ComboboxItemIndicator,
    ComboboxList,
    ComboboxTrigger,
    NdIcon,
  ],
  template: `
    <ng-container ndCombobox [(value)]="country">
      <div ndComboboxControl class="w-[240px]">
        <input
          ndComboboxInput
          aria-label="Country"
          placeholder="Select a country"
        />
        <button ndComboboxTrigger><svg ndIcon="chevron-down"></svg></button>
        <button ndComboboxClear><svg ndIcon="x"></svg></button>
      </div>
      <div ndComboboxContent>
        <div ndComboboxList>
          <div ndComboboxEmpty>No country found.</div>
          @for (name of countries; track name) {
            <div ndComboboxItem [value]="name">
              {{ name }}
              <span ndComboboxItemIndicator><svg ndIcon="check"></svg></span>
            </div>
          }
        </div>
      </div>
    </ng-container>
  `,
})
export class ComboboxClearExample {
  protected readonly country = signal<string | null>('Cameroon');
  protected readonly countries = [
    'Brazil',
    'Cameroon',
    'Canada',
    'France',
    'Japan',
    'Nigeria',
  ];
}

Combobox multiple

Several frameworks, chosen as chips.

Angular
import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Combobox,
  ComboboxChip,
  ComboboxChipRemove,
  ComboboxChips,
  ComboboxChipsInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxItemIndicator,
  ComboboxList,
} from '@/ui/combobox/combobox';

/**
 * Several frameworks, chosen as chips. Picking an option toggles it and
 * keeps the list open; Backspace in the empty field removes the last chip.
 */
@Component({
  selector: 'example-combobox-multiple',
  imports: [
    Combobox,
    ComboboxChip,
    ComboboxChipRemove,
    ComboboxChips,
    ComboboxChipsInput,
    ComboboxContent,
    ComboboxEmpty,
    ComboboxItem,
    ComboboxItemIndicator,
    ComboboxList,
    NdIcon,
  ],
  template: `
    <ng-container ndCombobox multiple [(values)]="chosen">
      <div ndComboboxChips class="w-xs max-w-full">
        @for (value of chosen(); track value) {
          <span ndComboboxChip [value]="value">
            {{ labelOf(value) }}
            <button ndComboboxChipRemove><svg ndIcon="x"></svg></button>
          </span>
        }
        <input
          ndComboboxChipsInput
          aria-label="Frameworks"
          placeholder="Add framework"
        />
      </div>
      <div ndComboboxContent>
        <div ndComboboxList>
          <div ndComboboxEmpty>No framework found.</div>
          @for (option of frameworks; track option.value) {
            <div ndComboboxItem [value]="option.value">
              {{ option.label }}
              <span ndComboboxItemIndicator><svg ndIcon="check"></svg></span>
            </div>
          }
        </div>
      </div>
    </ng-container>
  `,
})
export class ComboboxMultipleExample {
  protected readonly chosen = signal<readonly string[]>(['angular']);
  protected readonly frameworks = [
    { value: 'angular', label: 'Angular' },
    { value: 'analog', label: 'Analog' },
    { value: 'astro', label: 'Astro' },
    { value: 'nuxt', label: 'Nuxt' },
    { value: 'remix', label: 'Remix' },
    { value: 'sveltekit', label: 'SvelteKit' },
  ];

  /**
   * The label of a chosen framework, for its chip.
   *
   * @param value - The framework's value.
   * @returns Its label.
   */
  protected labelOf(value: string): string {
    return (
      this.frameworks.find((option) => option.value === value)?.label ?? value
    );
  }
}

Combobox groups

Options in labelled groups, with separators.

import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Combobox,
  ComboboxContent,
  ComboboxControl,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxInput,
  ComboboxItem,
  ComboboxItemIndicator,
  ComboboxLabel,
  ComboboxList,
  ComboboxSeparator,
  ComboboxTrigger,
} from '@/ui/combobox/combobox';

/**
 * Options in labelled groups, with separators. A group hides while none
 * of its options matches.
 */
@Component({
  selector: 'example-combobox-groups',
  imports: [
    Combobox,
    ComboboxContent,
    ComboboxControl,
    ComboboxEmpty,
    ComboboxGroup,
    ComboboxInput,
    ComboboxItem,
    ComboboxItemIndicator,
    ComboboxLabel,
    ComboboxList,
    ComboboxSeparator,
    ComboboxTrigger,
    NdIcon,
  ],
  template: `
    <ng-container ndCombobox>
      <div ndComboboxControl class="w-[240px]">
        <input ndComboboxInput aria-label="Food" placeholder="Select a food" />
        <button ndComboboxTrigger><svg ndIcon="chevron-down"></svg></button>
      </div>
      <div ndComboboxContent>
        <div ndComboboxList>
          <div ndComboboxEmpty>No food found.</div>
          @for (group of groups; track group.name; let last = $last) {
            <div ndComboboxGroup>
              <div ndComboboxLabel>{{ group.name }}</div>
              @for (food of group.foods; track food) {
                <div ndComboboxItem [value]="food">
                  {{ food }}
                  <span ndComboboxItemIndicator>
                    <svg ndIcon="check"></svg>
                  </span>
                </div>
              }
            </div>
            @if (!last) {
              <div ndComboboxSeparator></div>
            }
          }
        </div>
      </div>
    </ng-container>
  `,
})
export class ComboboxGroupsExample {
  protected readonly groups = [
    { name: 'Fruits', foods: ['Apple', 'Banana', 'Mango'] },
    { name: 'Vegetables', foods: ['Carrot', 'Okra', 'Spinach'] },
    { name: 'Grains', foods: ['Millet', 'Rice', 'Sorghum'] },
  ];
}

Combobox disabled

A disabled combobox, and one with a disabled option.

import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Combobox,
  ComboboxContent,
  ComboboxControl,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
} from '@/ui/combobox/combobox';

/**
 * A disabled combobox, and one with a disabled option, which the arrow
 * keys skip.
 */
@Component({
  selector: 'example-combobox-disabled',
  imports: [
    Combobox,
    ComboboxContent,
    ComboboxControl,
    ComboboxEmpty,
    ComboboxInput,
    ComboboxItem,
    ComboboxList,
    ComboboxTrigger,
    NdIcon,
  ],
  template: `
    <div class="flex flex-wrap items-center gap-4">
      <ng-container ndCombobox disabled>
        <div ndComboboxControl class="w-[200px]">
          <input
            ndComboboxInput
            aria-label="Team"
            placeholder="Select a team"
          />
          <button ndComboboxTrigger><svg ndIcon="chevron-down"></svg></button>
        </div>
        <div ndComboboxContent>
          <div ndComboboxList>
            <div ndComboboxEmpty>No team found.</div>
            <div ndComboboxItem value="design">Design</div>
          </div>
        </div>
      </ng-container>
      <ng-container ndCombobox>
        <div ndComboboxControl class="w-[200px]">
          <input
            ndComboboxInput
            aria-label="Region"
            placeholder="Select a region"
          />
          <button ndComboboxTrigger><svg ndIcon="chevron-down"></svg></button>
        </div>
        <div ndComboboxContent>
          <div ndComboboxList>
            <div ndComboboxEmpty>No region found.</div>
            <div ndComboboxItem value="africa">Africa</div>
            <div ndComboboxItem value="europe">Europe</div>
            <div ndComboboxItem value="antarctica" disabled>
              Antarctica (unavailable)
            </div>
          </div>
        </div>
      </ng-container>
    </div>
  `,
})
export class ComboboxDisabledExample {}

Accessibility

Implements the Editable combobox with list autocomplete pattern of the WAI-ARIA Authoring Practices.

Keyboard

Keys the component handles.

Key Action
aTypes into the field, opening the popup and filtering its options.
ArrowDownOpens the popup and highlights the chosen option or the first; while open, highlights the next option.
ArrowUpOpens the popup and highlights the last option; while open, highlights the previous option.
Alt+ArrowDownOpens the popup without highlighting an option.
EnterChooses the highlighted option. Without multiple the popup closes; with it, the option toggles and it stays open.
EscapeCloses the popup; if it's closed, clears the field and, without multiple, the value.
BackspaceWith multiple, in an empty field, removes the last chosen value.
HomeorEndMove the caret in the field, as in any text field.
TabCloses the popup and moves focus on.

Focus

  • Focus stays in the field; aria-activedescendant points at the highlighted option, which scrolls into view.
  • Clicking an option, the trigger, the clear button or a chip's remove button keeps focus in the field.
  • Focus leaving the combobox closes the popup and 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

Combobox

[ndCombobox]

A combobox (@ndop/primitives/combobox): a text field that filters a list of options below it as the user types. With multiple, chosen options show as chips. Put it on an ng-container, and bind forms on its field.

Combobox binding Type Default
[multiple]
Lets several options be chosen, shown as chips.
booleanfalse
[filter]
Tests whether an option matches what was typed; null shows every option, for lists filtered elsewhere.
NdFilter | nullndContainsFilter
[autoHighlight]
Highlights the first shown option as the user types.
booleanfalse
[disabled]
Disables the field. A disabled bound field does too.
booleanfalse
[required]
Marks the combobox required: aria-required="true" on the field.
booleanfalse
[invalid]
Marks the combobox invalid when no forms library is bound; with one, the field's state decides.
booleanfalse
[(value)]
The chosen option's value, or null, without multiple. Two-way bindable with [(value)].
string | nullnull
[(values)]
The chosen options' values, with multiple. Two-way bindable with [(values)].
readonly string[][]
[(search)]
The field's text. Two-way bindable with [(search)].
string''
[(open)]
Whether the popup is open. Two-way bindable with [(open)].
booleanfalse

ComboboxControl

[ndComboboxControl]

The bordered row around a single-value combobox's field and its buttons. The trigger hides while the clear button shows.

ComboboxControl binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcombobox-control

ComboboxInput

input[ndComboboxInput]role combobox

The text field of a single-value combobox, inside its ComboboxControl.

Manages aria-autocomplete, aria-expanded, aria-controls, aria-activedescendant, aria-required, aria-invalid, aria-describedby.

ComboboxInput binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcombobox-input
data-state —
data-disabled —
data-invalid —

ComboboxTrigger

button[ndComboboxTrigger]

The button that opens and closes the list with the pointer. Put the chevron icon in it.

Manages aria-hidden, tabindex.

ComboboxTrigger binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcombobox-trigger
data-state —

ComboboxClear

button[ndComboboxClear]

The button that empties the field and the value with the pointer. Put the x icon in it.

Manages aria-hidden, tabindex, hidden.

ComboboxClear binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcombobox-clear

ComboboxContent

[ndComboboxContent]

The popup, written after the field; it opens below the field or the chips, as wide as them.

Manages hidden.

ComboboxContent binding Type Default
[class]
Extra classes, merged last so they win.
string''
[side]
Side of the anchor it opens on, before flipping.
'bottom' | 'top' | 'start' | 'end''bottom'
[align]
Alignment along that side.
'start' | 'end' | 'center''start'
[sideOffset]
Gap between the anchor and the popup, in pixels.
number6
Data attribute Values
data-slotcombobox-content
data-state —
data-side —
data-empty —

ComboboxList

[ndComboboxList]role listbox

The scrolling list of options.

Manages aria-labelledby, aria-label, aria-multiselectable, id.

ComboboxList binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcombobox-list
data-empty —

ComboboxItem

[ndComboboxItem]role option

One option. Put its ComboboxItemIndicator last.

Manages aria-selected, aria-disabled, id, hidden.

ComboboxItem binding Type Default
[class]
Extra classes, merged last so they win.
string''
[value]required
The value choosing it gives the combobox.
string—
[disabled]
Makes it unchoosable; the arrow keys skip it.
booleanfalse
[label]
Its text, when it differs from what it renders.
string | nullnull
Data attribute Values
data-slotcombobox-item
data-highlighted —
data-selected —
data-disabled —

ComboboxItemIndicator

[ndComboboxItemIndicator]

The check of a chosen option, at its end.

Manages hidden.

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

ComboboxGroup

[ndComboboxGroup]role group

Groups options, named by its ComboboxLabel.

Manages aria-labelledby, hidden.

Data attribute Values
data-slotcombobox-group

ComboboxLabel

[ndComboboxLabel]

The heading of a ComboboxGroup.

Manages id.

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

ComboboxEmpty

[ndComboboxEmpty]

What shows while no option matches the field's text.

Manages hidden.

ComboboxEmpty binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcombobox-empty

ComboboxSeparator

[ndComboboxSeparator]

A line between groups of options.

Manages aria-hidden.

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

ComboboxChips

[ndComboboxChips]

The bordered box around the chips and the field of a combobox with multiple.

ComboboxChips binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcombobox-chips

ComboboxChip

[ndComboboxChip]

One chosen value, as a chip. Put its ComboboxChipRemove last.

ComboboxChip binding Type Default
[class]
Extra classes, merged last so they win.
string''
[value]required
The chosen value it shows.
string—
Data attribute Values
data-slotcombobox-chip
data-disabled —

ComboboxChipRemove

button[ndComboboxChipRemove]

The button that removes its chip's value with the pointer. Put the x icon in it.

Manages aria-hidden, tabindex.

ComboboxChipRemove binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcombobox-chip-remove

ComboboxChipsInput

input[ndComboboxChipsInput]role combobox

The text field among the chips of a combobox with multiple.

Manages aria-autocomplete, aria-expanded, aria-controls, aria-activedescendant, aria-required, aria-invalid, aria-describedby.

ComboboxChipsInput binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcombobox-chip-input
data-state —
data-disabled —
data-invalid —

Agent notes

Do

  • Name the field with a label, as any form field.
  • Show an empty state for when nothing matches.
  • Pass [filter]="null" and filter yourself when options come from a server.

Don't

  • Don't use it for a handful of options; use a Select or a Radio Group.
  • Don't use it to run commands; use Command.