Search documentation

Type to search the documentation.

Primitives

Listbox

A headless WAI-ARIA listbox for single and multiple selection. Quartz owns keyboard behaviour; your design system owns every visual detail.

At a glance

  • Keyboard — Arrows, Home/End, Enter/Space and type-ahead
  • Signals — Reactive value model with two-way binding
  • Accessible — Active-descendant and option semantics included
  • Composable — Foundation for select, combobox and command palettes

Sections and headers

Wrap options in qzListboxGroup with a qzListboxGroupLabel, and separate sections with qzListboxSeparator. Labels and separators are not options: keyboard navigation and typeahead skip them, DOM order is preserved, and dynamically added or removed sections re-register in place.

<div qzListbox aria-label="Fruit" [(value)]="fruit">
  <div qzListboxGroup>
    <div qzListboxGroupLabel>Citrus</div>
    <div qzListboxOption="lemon">Lemon</div>
    <div qzListboxOption="lime" qzListboxOptionDisabled>Lime</div>
  </div>
  <div qzListboxSeparator></div>
  <div qzListboxGroup>
    <div qzListboxGroupLabel>Berries</div>
    <div qzListboxOption="fig">Fig</div>
  </div>
</div>

Listbox

A real qzListbox with sections: single or multiple selection, disabled.

Citrus
Lemon
Lime
Orange
Berries
Blueberry
Strawberry

Value: null

Inputs

Events

No events yet. Interact with the preview to see them here.

Listbox — expanded preview

Value contract

The Quartz listbox keeps its own representation: a scalar in single mode and an array with multiple. An adapter for a library that always uses arrays translates at its boundary; Quartz does not force an array on single mode. valueChangeCommitted fires once per user change and never for programmatic writes. Selection and the active option are independent: moving with the arrows does not select.

Group labels are named through aria-labelledby, disabled options are skipped by navigation and keep aria-disabled. Horizontal listboxes follow reading direction (RTL mirrors ArrowLeft/ArrowRight).

Forms

ListboxControlDirective writes silently and notifies once per commit: scalar (single) or array (multiple), null for none. Touched fires when focus leaves the listbox.

Live examples

Loading demo…

Code

Single selection

import { signal } from '@angular/core';
import { ListboxDirective, ListboxOptionDirective } from '@quartz-headless/primitives';

selected = signal<string | null>('starter');

// All visual styles are yours.
<div qzListbox [value]="selected()" (valueChange)="selected.set($event)">
  <button qzListboxOption="starter">Starter</button>
  <button qzListboxOption="pro">Pro</button>
  <button qzListboxOption="enterprise">Enterprise</button>
</div>

Multiple selection

<div qzListbox [multiple]="true" [value]="tags()" (valueChange)="tags.set($event)">
  <button qzListboxOption="angular">Angular</button>
  <button qzListboxOption="signals">Signals</button>
  <button qzListboxOption="ssr" [qzListboxOptionDisabled]="true">SSR (unavailable)</button>
</div>

Angular Forms

import { ListboxDirective, ListboxOptionDirective } from '@quartz-headless/primitives';
import { ListboxControlDirective } from '@quartz-headless/primitives/forms';

// <div qzListbox [multiple]="true" formControlName="tags" aria-label="Tags">
//   <div qzListboxOption="alpha">Alpha</div> ...
// </div>
// value: T (single) | T[] (multiple); reset() writes null = nothing selected.