Primitives
A listbox presents a set of choices and lets users select one or multiple options. It can be paired with the NgpPopover
directive to create a dropdown listbox.
Select a character from the list below.import { Component, signal } from "@angular/core";
import { NgIcon, provideIcons } from "@ng-icons/core";
import {
heroCheckSolid,
heroChevronDownSolid,
} from "@ng-icons/heroicons/solid";
import { NgpButton } from "ng-primitives/button";
import {
NgpDescription,
NgpFormField,
NgpLabel,
} from "ng-primitives/form-field";
import {
NgpListbox,
NgpListboxOption,
NgpListboxTrigger,
} from "ng-primitives/listbox";
import { NgpPopover, NgpPopoverTrigger } from "ng-primitives/popover";
@Component({
selector: "app-listbox-select",
imports: [
NgpListbox,
NgpLabel,
NgpDescription,
NgpListboxOption,
NgpButton,
NgpFormField,
NgpPopover,
NgpPopoverTrigger,
NgpListboxTrigger,
NgIcon,
],
providers: [provideIcons({ heroCheckSolid, heroChevronDownSolid })],
template: `
<div ngpFormField>
<label ngpLabel>Character</label>
<p ngpDescription>Select a character from the list below.</p>
<button [ngpPopoverTrigger]="dropdown" ngpButton ngpListboxTrigger>
{{ selection()[0].name }}
<ng-icon name="heroChevronDownSolid" />
</button>
<ng-template #dropdown>
<div
[(ngpListboxValue)]="selection"
ngpPopover
ngpListbox
aria-label="Characters"
>
@for (option of options; track option.id) {
<div [ngpListboxOptionValue]="option" ngpListboxOption>
<ng-icon name="heroCheckSolid" size="16px" />
{{ option.name }}
</div>
}
</div>
</ng-template>
</div>
`,
styles: `
[ngpFormField] {
display: flex;
flex-direction: column;
gap: 6px;
width: 90%;
}
[ngpLabel] {
color: var(--ngp-text-primary);
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
margin: 0;
}
[ngpDescription] {
color: var(--ngp-text-secondary);
font-size: 0.75rem;
line-height: 1rem;
margin: 0 0 4px;
}
[ngpButton] {
display: flex;
justify-content: space-between;
align-items: center;
height: 36px;
width: 300px;
border-radius: 8px;
padding: 0 16px;
border: none;
background-color: var(--ngp-background);
text-align: left;
box-shadow: var(--ngp-input-shadow);
outline: none;
}
[ngpButton][data-focus-visible] {
outline: 2px solid var(--ngp-focus-ring);
outline-offset: 2px;
}
[ngpListbox] {
background-color: var(--ngp-background);
border: 1px solid var(--ngp-border);
padding: 0.25rem;
border-radius: 0.75rem;
outline: none;
position: absolute;
animation: popover-show 0.1s ease-out;
width: var(--ngp-popover-trigger-width);
box-shadow: var(--ngp-shadow-lg);
}
[ngpListboxOption] {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.75rem;
cursor: pointer;
border-radius: 0.5rem;
width: 100%;
height: 36px;
box-sizing: border-box;
}
[ngpListboxOption][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpListboxOption][data-press] {
background-color: var(--ngp-background-active);
}
[ngpListboxOption][data-active] {
background-color: var(--ngp-background-active);
}
[ngpListboxOption] ng-icon {
visibility: hidden;
}
[ngpListboxOption][data-selected] ng-icon {
visibility: visible;
}
@keyframes popover-show {
0% {
opacity: 0;
transform: translateY(-10px) scale(0.9);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
`,
})
export default class ListboxSelectExample {
readonly options: Option[] = [
{ id: 1, name: "Marty McFly" },
{ id: 2, name: "Doc Brown" },
{ id: 3, name: "Biff Tannen" },
{ id: 4, name: "Lorraine Baines" },
{ id: 5, name: "George McFly" },
];
readonly selection = signal<Option[]>([this.options[0]]);
}
interface Option {
id: number;
name: string;
}
Import the Listbox primitives from ng-primitives/listbox
.
import { NgpListbox } from 'ng-primitives/listbox';
Assemble the listbox directives in your template.
<div ngpListbox>
<div ngpListboxOption ngpListboxOptionValue="1">Option 1</div>
<div ngpListboxOption ngpListboxOptionValue="2">Option 2</div>
<div ngpListboxOption ngpListboxOptionValue="3">Option 3</div>
</div>
Create a reusable component that uses the listbox directives.
import { Component } from '@angular/core';
import { Listbox } from './listbox';
import { ListboxOption } from './listbox-option';
@Component({
selector: 'app-root',
imports: [Listbox, ListboxOption],
template: `
<app-listbox>
<app-listbox-option value="One">One</app-listbox-option>
<app-listbox-option value="Two">Two</app-listbox-option>
<app-listbox-option value="Three">Three</app-listbox-option>
</app-listbox>
`,
})
export default class App {}
Generate a reusable listbox component using the Angular CLI.
ng g ng-primitives:primitive listbox
path
: The path at which to create the component file.prefix
: The prefix to apply to the generated component selector.componentSuffix
: The suffix to apply to the generated component class name.fileSuffix
: The suffix to apply to the generated component file name. Defaults to component
.exampleStyles
: Whether to include example styles in the generated component file. Defaults to true
.The listbox can be configured to allow a single selection.
import { Component, signal } from "@angular/core";
import { NgIcon, provideIcons } from "@ng-icons/core";
import { heroCheckSolid } from "@ng-icons/heroicons/solid";
import { NgpListbox, NgpListboxOption } from "ng-primitives/listbox";
@Component({
selector: "app-listbox",
imports: [NgpListbox, NgpListboxOption, NgIcon],
providers: [provideIcons({ heroCheckSolid })],
template: `
<div [(ngpListboxValue)]="selection" ngpListbox aria-label="Characters">
@for (option of options; track option.id) {
<div [ngpListboxOptionValue]="option" ngpListboxOption>
<ng-icon name="heroCheckSolid" size="16px" />
{{ option.name }}
</div>
}
</div>
`,
styles: `
[ngpListbox] {
background-color: var(--ngp-background);
border: 1px solid var(--ngp-border);
padding: 0.25rem;
border-radius: 0.75rem;
outline: none;
}
[ngpListboxOption] {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.75rem;
cursor: pointer;
border-radius: 0.5rem;
width: 200px;
height: 36px;
box-sizing: border-box;
}
[ngpListboxOption][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpListboxOption][data-press] {
background-color: var(--ngp-background-active);
}
[ngpListboxOption][data-active] {
background-color: var(--ngp-background-active);
}
[ngpListboxOption] ng-icon {
visibility: hidden;
}
[ngpListboxOption][data-selected] ng-icon {
visibility: visible;
}
`,
})
export default class ListboxExample {
readonly options: Option[] = [
{ id: 1, name: "Marty McFly" },
{ id: 2, name: "Doc Brown" },
{ id: 3, name: "Biff Tannen" },
{ id: 4, name: "Lorraine Baines" },
{ id: 5, name: "George McFly" },
];
readonly selection = signal<Option[]>([this.options[0]]);
}
interface Option {
id: number;
name: string;
}
The listbox can be configured to allow multiple selections.
import { Component, signal } from "@angular/core";
import { NgIcon, provideIcons } from "@ng-icons/core";
import { heroCheckSolid } from "@ng-icons/heroicons/solid";
import { NgpListbox, NgpListboxOption } from "ng-primitives/listbox";
@Component({
selector: "app-listbox-multiple",
imports: [NgpListbox, NgpListboxOption, NgIcon],
providers: [provideIcons({ heroCheckSolid })],
template: `
<div [(ngpListboxValue)]="selection" ngpListbox ngpListboxMode="multiple">
@for (option of options; track option.id) {
<div [ngpListboxOptionValue]="option" ngpListboxOption>
<ng-icon name="heroCheckSolid" size="16px" />
{{ option.name }}
</div>
}
</div>
`,
styles: `
[ngpListbox] {
background-color: var(--ngp-background);
border: 1px solid var(--ngp-border);
padding: 0.25rem;
border-radius: 0.75rem;
outline: none;
}
[ngpListboxOption] {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.75rem;
cursor: pointer;
border-radius: 0.5rem;
width: 200px;
height: 36px;
box-sizing: border-box;
}
[ngpListboxOption][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpListboxOption][data-press] {
background-color: var(--ngp-background-active);
}
[ngpListboxOption][data-active] {
background-color: var(--ngp-background-active);
}
[ngpListboxOption] ng-icon {
visibility: hidden;
}
[ngpListboxOption][data-selected] ng-icon {
visibility: visible;
}
`,
})
export default class ListboxMultipleExample {
readonly options: Option[] = [
{ id: 1, name: "Marty McFly" },
{ id: 2, name: "Doc Brown" },
{ id: 3, name: "Biff Tannen" },
{ id: 4, name: "Lorraine Baines" },
{ id: 5, name: "George McFly" },
];
readonly selection = signal<Option[]>([this.options[0], this.options[1]]);
}
interface Option {
id: number;
name: string;
}
The listbox can be configured to have sections and headers.
import { Component, signal } from "@angular/core";
import { NgIcon, provideIcons } from "@ng-icons/core";
import { heroCheckSolid } from "@ng-icons/heroicons/solid";
import { NgpHeader } from "ng-primitives/common";
import {
NgpListbox,
NgpListboxOption,
NgpListboxSection,
} from "ng-primitives/listbox";
@Component({
selector: "app-listbox-sections",
imports: [NgpListbox, NgpListboxOption, NgpListboxSection, NgpHeader, NgIcon],
providers: [provideIcons({ heroCheckSolid })],
template: `
<div [(ngpListboxValue)]="selection" ngpListbox aria-label="Sections">
@for (section of sections; track section.name) {
<header class="listbox-header" ngpHeader>{{ section.name }}</header>
<div ngpListboxSection>
@for (option of section.options; track option.id) {
<div [ngpListboxOptionValue]="option" ngpListboxOption>
<ng-icon name="heroCheckSolid" size="16px" />
{{ option.name }}
</div>
}
</div>
}
</div>
`,
styles: `
[ngpListbox] {
background-color: var(--ngp-background);
border: 1px solid var(--ngp-border);
padding: 0.25rem;
border-radius: 0.75rem;
list-style: none;
outline: none;
max-height: 300px;
overflow-y: auto;
}
.listbox-header {
padding: 0.25rem 0.75rem;
font-weight: 600;
}
[ngpListboxOption] {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.75rem;
cursor: pointer;
border-radius: 0.5rem;
width: 200px;
height: 36px;
box-sizing: border-box;
}
[ngpListboxOption][data-hover] {
background-color: var(--ngp-background-hover);
}
[ngpListboxOption][data-press] {
background-color: var(--ngp-background-active);
}
[ngpListboxOption][data-active] {
background-color: var(--ngp-background-active);
}
[ngpListboxOption] ng-icon {
visibility: hidden;
}
[ngpListboxOption][data-selected] ng-icon {
visibility: visible;
}
`,
})
export default class ListboxSectionsExample {
readonly sections: Section[] = [
{
name: "Characters",
options: [
{ id: 1, name: "Marty McFly" },
{ id: 2, name: "Doc Brown" },
{ id: 3, name: "Biff Tannen" },
{ id: 4, name: "Lorraine Baines" },
{ id: 5, name: "George McFly" },
],
},
{
name: "Locations",
options: [
{ id: 6, name: "Hill Valley" },
{ id: 7, name: "Twin Pines Mall" },
{ id: 8, name: "Lyon Estates" },
],
},
{
name: "Items",
options: [
{ id: 9, name: "DeLorean" },
{ id: 10, name: "Hoverboard" },
{ id: 11, name: "Sports Almanac" },
{ id: 13, name: "Plutonium" },
{ id: 14, name: "Mr. Fusion" },
{ id: 15, name: "Flux Capacitor" },
],
},
];
readonly selection = signal<Option[]>([this.sections[0].options[0]]);
}
interface Section {
name: string;
options: Option[];
}
interface Option {
id: number;
name: string;
}
The following directives are available to import from the ng-primitives/listbox
package:
The listbox selection mode.
The listbox selection.
The listbox disabled state.
The comparator function to use when comparing values.
If not provided, strict equality (===) is used.
Emits when the listbox selection changes.
[ngpListbox]
ngpListbox
The following data attributes are applied to the ngpListbox
directive:
Attribute | Description |
---|---|
data-focus-visible |
Applied to the listbox when focused via the keyboard. |
The value of the option.
Whether the option is disabled.
[ngpListboxOption]
ngpListboxOption
The following data attributes are applied to the ngpListboxOption
directive:
Attribute | Description |
---|---|
data-hover |
Applied to the listbox option when hovered. |
data-focus |
Applied to the listbox option when focused. |
data-focus-visible |
Applied to the listbox option when focused via the keyboard. |
data-disabled |
Applied to the listbox option when disabled. |
data-active |
Applied to the listbox option when it is the active descendant. |
data-disabled |
Applied to the listbox option when it is disabled. |
data-selected |
Applied to the listbox option when it is selected. |
[ngpListboxSection]
ngpListboxSection
[ngpListboxTrigger]
ngpListboxTrigger
Augments the popover trigger with listbox-specific behavior, such as opening the listbox when the arrow keys are pressed.
Adheres to the WAI-ARIA Listbox Design Pattern.
Copyright © 2025 Angular Primitives