Listbox pattern
Listbox API Reference
Директива, отображающая список опций для выбора пользователем, с поддержкой клавиатурной навигации, одиночного или множественного выбора и screen reader.
import { Listbox , Option } from '@angular/aria/listbox' ;
import { Component } from '@angular/core' ;
@ Component ({
selector: 'app-root' ,
templateUrl: './app.html' ,
styleUrl: './app.css' ,
imports: [ Listbox , Option ],
})
export class App {
/** The options available in the listbox. */
options = [
'Option 1' ,
'Option 2' ,
'Option 3' ,
'Option 4' ,
'Option 5' ,
'Option 6' ,
'Option 7' ,
'Option 8' ,
];
}
< div class = "listbox-container" >
< div ngListbox [value] = "['Option 1']" >
@for ( option of options; track option) {
< div ngOption [value] = "option" >
< span class = "example-option-text" >{{ option }}</ span >
< span
class = "example-option-check material-symbols-outlined"
translate = "no"
aria-hidden = "true"
>check</ span
>
</ div >
}
</ div >
</ div >
@import url ( 'https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined' );
:host {
display : flex ;
justify-content : center ;
font-family : var ( --inter-font );
}
.listbox-container {
width : 200 px ;
height : 11 rem ;
padding : 0.5 rem ;
border-radius : 0.5 rem ;
background-color : var ( --septenary-contrast );
font-size : 0.9 rem ;
}
[ ngListbox ] {
gap : 2 px ;
height : 100 % ;
display : flex ;
overflow : auto ;
flex-direction : column ;
}
[ ngOption ] {
display : flex ;
cursor : pointer ;
align-items : center ;
margin : 1 px ;
padding : 0 1 rem ;
min-height : 2.25 rem ;
border-radius : 0.5 rem ;
}
[ ngOption ] :hover {
background-color : color-mix ( in srgb , var ( --primary-contrast ) 5 % , transparent );
}
[ ngOption ][ data-active = 'true' ] {
outline-offset : -2 px ;
outline : 2 px solid color-mix ( in srgb , var ( --hot-pink ) 50 % , transparent );
}
[ ngOption ][ aria-selected = 'true' ] {
color : var ( --hot-pink );
background-color : color-mix ( in srgb , var ( --hot-pink ) 5 % , transparent );
}
[ ngOption ] :not ([ aria-selected = 'true' ]) .example-option-check {
display : none ;
}
.example-option-check {
font-size : 0.9 rem ;
}
.example-option-text {
flex : 1 ;
}
import { Listbox , Option } from '@angular/aria/listbox' ;
import { Component } from '@angular/core' ;
@ Component ({
selector: 'app-root' ,
templateUrl: './app.html' ,
styleUrl: './app.css' ,
imports: [ Listbox , Option ],
})
export class App {
/** The options available in the listbox. */
options = [
'Option 1' ,
'Option 2' ,
'Option 3' ,
'Option 4' ,
'Option 5' ,
'Option 6' ,
'Option 7' ,
'Option 8' ,
];
}
< div class = "material-listbox" >
< div ngListbox >
@for ( option of options; track option) {
< div ngOption [value] = "option" >
< span class = "example-option-text" >{{ option }}</ span >
< span
class = "example-option-check material-symbols-outlined"
translate = "no"
aria-hidden = "true"
>check</ span
>
</ div >
}
</ div >
</ div >
@import url ( 'https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined' );
:host {
display : flex ;
justify-content : center ;
font-family : var ( --inter-font );
--primary : var ( --hot-pink );
--on-primary : var ( --page-background );
}
.docs-light-mode {
--on-primary : #fff ;
}
.material-listbox {
width : 200 px ;
height : 13 rem ;
padding : 0.5 rem ;
border-radius : 2 rem ;
background-color : var ( --septenary-contrast );
font-size : 0.9 rem ;
}
[ ngListbox ] {
gap : 2 px ;
padding : 2 px ;
height : 100 % ;
display : flex ;
overflow : auto ;
flex-direction : column ;
}
[ ngOption ] {
display : flex ;
cursor : pointer ;
align-items : center ;
padding : 0 1 rem ;
min-height : 3 rem ;
border-radius : 3 rem ;
}
[ ngOption ] :hover ,
[ ngOption ][ data-active = 'true' ] {
background-color : color-mix ( in srgb , var ( --primary-contrast ) 5 % , transparent );
}
[ ngOption ][ data-active = 'true' ] {
outline-offset : -2 px ;
outline : 2 px solid var ( --primary );
}
[ ngOption ][ aria-selected = 'true' ] {
color : var ( --primary );
background-color : color-mix ( in srgb , var ( --primary ) 10 % , transparent );
}
[ ngOption ] :not ([ aria-selected = 'true' ]) .example-option-check {
display : none ;
}
.example-option-check {
font-size : 0.9 rem ;
}
.example-option-text {
flex : 1 ;
}
import { Listbox , Option } from '@angular/aria/listbox' ;
import { Component } from '@angular/core' ;
@ Component ({
selector: 'app-root' ,
templateUrl: './app.html' ,
styleUrl: './app.css' ,
imports: [ Listbox , Option ],
})
export class App {
/** The options available in the listbox. */
options = [
'Option 1' ,
'Option 2' ,
'Option 3' ,
'Option 4' ,
'Option 5' ,
'Option 6' ,
'Option 7' ,
'Option 8' ,
];
}
< div class = "retro-listbox" >
< div ngListbox >
@for ( option of options; track option) {
< div ngOption [value] = "option" >
< span class = "example-option-text" >{{ option }}</ span >
< span
class = "example-option-check material-symbols-outlined"
translate = "no"
aria-hidden = "true"
>check</ span
>
</ div >
}
</ div >
</ div >
@import url ( 'https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined' );
@import url ( 'https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap' );
:host {
display : flex ;
justify-content : center ;
font-size : 0.8 rem ;
font-family : 'Press Start 2P' ;
--retro-button-color : color-mix ( in srgb , var ( --hot-pink ) 80 % , var ( --page-background ));
--retro-shadow-light : color-mix ( in srgb , var ( --retro-button-color ) 90 % , #fff );
--retro-shadow-dark : color-mix ( in srgb , var ( --retro-button-color ) 90 % , #000 );
--retro-flat-shadow :
4 px 0 px 0 px 0 px var ( --tertiary-contrast ), 0 px 4 px 0 px 0 px var ( --tertiary-contrast ),
-4 px 0 px 0 px 0 px var ( --tertiary-contrast ), 0 px -4 px 0 px 0 px var ( --tertiary-contrast );
}
.retro-listbox {
width : 200 px ;
height : 11 rem ;
padding : 0.5 rem ;
box-shadow : var ( --retro-flat-shadow );
background-color : var ( --septenary-contrast );
}
[ ngListbox ] {
gap : 2 px ;
height : 100 % ;
display : flex ;
overflow : auto ;
flex-direction : column ;
}
[ ngOption ] {
display : flex ;
cursor : pointer ;
align-items : center ;
padding : 0 1 rem ;
font-size : 0.6 rem ;
min-height : 2.25 rem ;
}
[ ngOption ] :hover {
background-color : color-mix ( in srgb , var ( --primary-contrast ) 5 % , transparent );
}
[ ngOption ][ data-active = 'true' ] {
outline-offset : -2 px ;
outline : 2 px dashed var ( --hot-pink );
}
[ ngOption ][ aria-selected = 'true' ] {
color : var ( --hot-pink );
background-color : color-mix ( in srgb , var ( --hot-pink ) 5 % , transparent );
}
[ ngOption ] :not ([ aria-selected = 'true' ]) .example-option-check {
display : none ;
}
.example-option-icon ,
.example-option-check {
font-size : 0.9 rem ;
}
.example-option-text {
flex : 1 ;
}
Listbox — базовая директива, используемая паттернами Select , Multiselect и Autocomplete . Для большинства нужд dropdown используйте эти задокументированные паттерны.
Рассмотрите прямое использование listbox, когда:
Создаёте кастомные компоненты выбора — специализированные интерфейсы с конкретным поведением
Видимые списки выбора — отображение выбираемых элементов прямо на странице (не в dropdown)
Кастомные паттерны интеграции — интеграция с уникальными требованиями popup или layout
Избегайте listbox, когда:
Нужны навигационные меню — используйте директиву Menu для действий и команд
Listbox Angular предоставляет полностью доступную реализацию списка с:
Клавиатурной навигацией — перемещение по опциям стрелками, выбор Enter или Space
Поддержкой screen reader — встроенные ARIA-атрибуты, включая role="listbox"
Одиночным или множественным выбором — атрибут multi управляет режимом выбора
Горизонтальной или вертикальной ориентацией — атрибут orientation для направления layout
Type-ahead поиском — ввод символов для перехода к совпадающим опциям
Signal-based реактивностью — реактивное управление состоянием через сигналы Angular
Иногда приложениям нужны выбираемые списки, видимые прямо на странице, а не скрытые в dropdown. Standalone listbox обеспечивает клавиатурную навигацию и выбор для таких видимых списковых интерфейсов.
import { Listbox , Option } from '@angular/aria/listbox' ;
import { Component } from '@angular/core' ;
@ Component ({
selector: 'app-root' ,
templateUrl: './app.html' ,
styleUrl: './app.css' ,
imports: [ Listbox , Option ],
})
export class App {
/** The options available in the listbox. */
options = [
'Option 1' ,
'Option 2' ,
'Option 3' ,
'Option 4' ,
'Option 5' ,
'Option 6' ,
'Option 7' ,
'Option 8' ,
];
}
< div class = "listbox-container" >
< div ngListbox [value] = "['Option 1']" >
@for ( option of options; track option) {
< div ngOption [value] = "option" >
< span class = "example-option-text" >{{ option }}</ span >
< span
class = "example-option-check material-symbols-outlined"
translate = "no"
aria-hidden = "true"
>check</ span
>
</ div >
}
</ div >
</ div >
Model-сигнал value обеспечивает двустороннюю привязку к выбранным элементам. С selectionMode="explicit" пользователи нажимают Space или Enter для выбора опций. Для паттернов dropdown, сочетающих listbox с combobox и позиционированием overlay, см. паттерн Select .
Иногда списки лучше работают горизонтально — например, интерфейсы в стиле toolbar или выбор в стиле вкладок. Атрибут orientation меняет и layout, и направление клавиатурной навигации.
import { Listbox , Option } from '@angular/aria/listbox' ;
import { Component } from '@angular/core' ;
@ Component ({
selector: 'app-root' ,
templateUrl: './app.html' ,
styleUrl: './app.css' ,
imports: [ Listbox , Option ],
})
export class App {
/** The options available in the listbox. */
amenities = [ 'Washer / Dryer' , 'Ramp access' , 'Garden' , 'Cats OK' , 'Dogs OK' , 'Smoke-free' ];
}
< div ngListbox aria-label = "Amenities" orientation = "horizontal" selectionMode = "explicit" multi >
@for ( amenity of amenities; track amenity) {
< div ngOption [value] = "amenity" [label] = "amenity" >
< span class = "option-label" >{{ amenity }}</ span >
</ div >
}
</ div >
@import url ( 'https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined' );
:host {
font-size : 0.8 rem ;
font-family : var ( --inter-font );
}
[ ngListbox ] {
gap : 0.5 rem ;
display : flex ;
flex-wrap : wrap ;
}
[ ngOption ] {
cursor : pointer ;
border-radius : 1 rem ;
padding : 0.3 rem 1 rem ;
color : var ( --hot-pink );
border : 1 px solid var ( --hot-pink );
background-color : color-mix ( in srgb , var ( --hot-pink ) 5 % , transparent );
}
[ ngOption ] :focus {
outline : 2 px solid var ( --hot-pink );
outline-offset : 2 px ;
}
[ ngOption ] :hover {
background-color : color-mix ( in srgb , var ( --hot-pink ) 15 % , transparent );
}
[ ngOption ][ aria-selected = 'true' ] {
color : var ( --page-background );
background-color : var ( --hot-pink );
}
import { Listbox , Option } from '@angular/aria/listbox' ;
import { Component } from '@angular/core' ;
@ Component ({
selector: 'app-root' ,
templateUrl: './app.html' ,
styleUrl: './app.css' ,
imports: [ Listbox , Option ],
})
export class App {
/** The options available in the listbox. */
amenities = [ 'Washer / Dryer' , 'Ramp access' , 'Garden' , 'Cats OK' , 'Dogs OK' , 'Smoke-free' ];
}
< div
ngListbox
class = "material-listbox"
aria-label = "Amenities"
orientation = "horizontal"
selectionMode = "explicit"
multi
>
@for ( amenity of amenities; track amenity) {
< div ngOption [value] = "amenity" [label] = "amenity" >
< span class = "check-icon material-symbols-outlined" translate = "no" aria-hidden = "true"
>check</ span
>
< span class = "option-label" >{{ amenity }}</ span >
</ div >
}
</ div >
@import url ( 'https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined' );
:host {
font-size : 0.8 rem ;
font-family : var ( --inter-font );
}
[ ngListbox ] {
gap : 0.5 rem ;
display : flex ;
flex-wrap : wrap ;
}
[ ngOption ] {
display : flex ;
cursor : pointer ;
align-items : center ;
border-radius : 0.3 rem ;
padding : 0.3 rem 0.5 rem ;
color : var ( --hot-pink );
border : 1 px solid var ( --hot-pink );
background-color : color-mix ( in srgb , var ( --hot-pink ) 5 % , transparent );
}
[ ngOption ] :focus {
outline : 2 px solid var ( --hot-pink );
outline-offset : 2 px ;
}
[ ngOption ] :hover {
background-color : color-mix ( in srgb , var ( --hot-pink ) 15 % , transparent );
}
[ ngOption ][ aria-selected = 'true' ] {
color : var ( --page-background );
background-color : var ( --hot-pink );
}
.check-icon {
width : 0 ;
font-size : 1.25 rem ;
overflow : hidden ;
transition :
width 0.2 s ease-in-out ,
padding-right 0.2 s ease-in-out ;
}
[ ngOption ][ aria-selected = 'true' ] .check-icon {
width : 1.5 rem ;
padding-right : 0.2 rem ;
}
import { Listbox , Option } from '@angular/aria/listbox' ;
import { Component } from '@angular/core' ;
@ Component ({
selector: 'app-root' ,
templateUrl: './app.html' ,
styleUrl: './app.css' ,
imports: [ Listbox , Option ],
})
export class App {
/** The options available in the listbox. */
amenities = [ 'Washer / Dryer' , 'Ramp access' , 'Garden' , 'Cats OK' , 'Dogs OK' , 'Smoke-free' ];
}
< div
ngListbox
class = "retro-listbox"
aria-label = "Amenities"
orientation = "horizontal"
selectionMode = "explicit"
multi
>
@for ( amenity of amenities; track amenity) {
< div ngOption [value] = "amenity" [label] = "amenity" >
< span class = "check-icon material-symbols-outlined" translate = "no" aria-hidden = "true"
>check</ span
>
< span class = "option-label" >{{ amenity }}</ span >
</ div >
}
</ div >
@import url ( 'https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined' );
@import url ( 'https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap' );
:host {
display : flex ;
justify-content : center ;
font-size : 0.8 rem ;
font-family : 'Press Start 2P' ;
--retro-button-color : color-mix ( in srgb , var ( --hot-pink ) 80 % , var ( --page-background ));
--retro-shadow-light : color-mix ( in srgb , var ( --retro-button-color ) 90 % , #fff );
--retro-shadow-dark : color-mix ( in srgb , var ( --retro-button-color ) 90 % , #000 );
--retro-flat-shadow :
4 px 0 px 0 px 0 px var ( --tertiary-contrast ), 0 px 4 px 0 px 0 px var ( --tertiary-contrast ),
-4 px 0 px 0 px 0 px var ( --tertiary-contrast ), 0 px -4 px 0 px 0 px var ( --tertiary-contrast );
--retro-flat-shadow-small :
2 px 0 px 0 px 0 px var ( --tertiary-contrast ), 0 px 2 px 0 px 0 px var ( --tertiary-contrast ),
-2 px 0 px 0 px 0 px var ( --tertiary-contrast ), 0 px -2 px 0 px 0 px var ( --tertiary-contrast );
}
.retro-listbox {
gap : 0.5 rem ;
display : flex ;
flex-wrap : wrap ;
padding : 0.5 rem ;
box-shadow : var ( --retro-flat-shadow );
background-color : var ( --septenary-contrast );
}
[ ngOption ] {
display : flex ;
cursor : pointer ;
align-items : center ;
padding : 0.3 rem 0.5 rem ;
font-size : 0.6 rem ;
box-shadow : var ( --retro-flat-shadow-small );
}
[ ngOption ] :hover {
background-color : color-mix ( in srgb , var ( --primary-contrast ) 5 % , transparent );
}
[ ngOption ][ data-active = 'true' ] {
outline-offset : -2 px ;
outline : 2 px dashed var ( --hot-pink );
}
[ ngOption ][ aria-selected = 'true' ] {
color : var ( --hot-pink );
background-color : color-mix ( in srgb , var ( --hot-pink ) 5 % , transparent );
}
.check-icon {
width : 0 ;
font-size : 0.9 rem ;
overflow : hidden ;
transition : width 0.2 s ease-in-out ;
}
[ ngOption ][ aria-selected = 'true' ] .check-icon {
width : 1.2 rem ;
}
С orientation="horizontal" клавиши влево и вправо перемещают между опциями вместо вверх и вниз. Listbox автоматически обрабатывает языки справа налево (RTL), меняя направление навигации.
Listbox поддерживает два режима выбора, управляющих тем, когда элементы становятся выбранными.
Режим 'follow' автоматически выбирает элемент в фокусе, обеспечивая более быстрое взаимодействие при частой смене выбора. Режим 'explicit' требует Space или Enter для подтверждения выбора, предотвращая случайные изменения при навигации. Паттерны dropdown обычно используют режим 'follow' для одиночного выбора.
import { Component } from '@angular/core' ;
import { Listbox , Option } from '@angular/aria/listbox' ;
@ Component ({
selector: 'app-root' ,
templateUrl: './app.html' ,
styleUrl: './app.css' ,
imports: [ Listbox , Option ],
})
export class App {
amenities = [ 'Washer / Dryer' , 'Ramp access' , 'Garden' , 'Cats OK' , 'Dogs OK' , 'Smoke-free' ];
}
< div
ngListbox
aria-label = "Amenities_explicit"
orientation = "horizontal"
selectionMode = "explicit"
multi
>
@for ( amenity of amenities; track amenity) {
< div ngOption [value] = "amenity" [label] = "amenity" >
< span class = "option-label" >{{ amenity }}</ span >
</ div >
}
</ div >
import { Component } from '@angular/core' ;
import { Listbox , Option } from '@angular/aria/listbox' ;
@ Component ({
selector: 'app-root' ,
templateUrl: './app.html' ,
styleUrl: './app.css' ,
imports: [ Listbox , Option ],
})
export class App {
amenities = [ 'Washer / Dryer' , 'Ramp access' , 'Garden' , 'Cats OK' , 'Dogs OK' , 'Smoke-free' ];
}
< div
ngListbox
aria-label = "Amenities_explicit"
orientation = "horizontal"
selectionMode = "follow"
multi
>
@for ( amenity of amenities; track amenity) {
< div ngOption [value] = "amenity" [label] = "amenity" >
< span class = "option-label" >{{ amenity }}</ span >
</ div >
}
</ div >
СОВЕТ: Паттерны dropdown обычно используют режим 'follow' для одиночного выбора.
Angular Aria предоставляет component harnesses для тестирования компонентов listbox.
Пример использования harnesses в тесте компонента:
import { ComponentFixture , TestBed } from '@angular/core/testing' ;
import { HarnessLoader } from '@angular/cdk/testing' ;
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed' ;
import {ListboxHarness} from '@angular/aria/listbox/testing' ;
import {MyListboxComponent} from './my-listbox' ; // Your component
describe ( 'MyListboxComponent' , () => {
let fixture : ComponentFixture < MyListboxComponent >;
let loader : HarnessLoader ;
beforeEach ( async () => {
TestBed . configureTestingModule ({
imports: [MyListboxComponent],
});
fixture = TestBed . createComponent (MyListboxComponent);
await fixture. whenStable ();
loader = TestbedHarnessEnvironment . loader (fixture);
});
it ( 'should allow selecting options' , async () => {
const listbox = await loader. getHarness (ListboxHarness);
// Verify listbox properties
expect ( await listbox. isMulti ()). toBe ( true );
// Get all options
const options = await listbox. getOptions ();
expect (options. length ). toBe ( 2 );
// Click an option
await options[ 0 ]. click ();
// Verify option is selected
expect ( await options[ 0 ]. isSelected ()). toBe ( true );
// Filter options by text
const bananaOption = await listbox. getOptions ({text: 'Banana' });
expect (bananaOption. length ). toBe ( 1 );
});
});
Подробную API-документацию смотрите в следующих API reference:
Listbox используется этими задокументированными паттернами dropdown:
Select — паттерн dropdown с одиночным выбором: readonly combobox + listbox
Multiselect — паттерн dropdown с множественным выбором: readonly combobox + listbox с multi
Autocomplete — паттерн filterable dropdown: combobox + listbox
Для полных паттернов dropdown с trigger, popup и позиционированием overlay см. руководства по этим паттернам вместо использования listbox отдельно.