Select
Overview
A pattern that combines readonly combobox with listbox to create single-selection dropdowns with keyboard navigation and screen reader support.
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The icon that is displayed in the combobox. */ displayIcon = computed(() => { const values = this.listbox()?.values() || []; const label = this.labels.find((label) => label.value === values[0]); return label ? label.icon : ''; }); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = [ {value: 'Important', icon: 'label'}, {value: 'Starred', icon: 'star'}, {value: 'Work', icon: 'work'}, {value: 'Personal', icon: 'person'}, {value: 'To Do', icon: 'checklist'}, {value: 'Later', icon: 'schedule'}, {value: 'Read', icon: 'menu_book'}, {value: 'Travel', icon: 'flight'}, ]; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox readonly> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-icon material-symbols-outlined">{{ displayIcon() }}</span> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label.value) { <div ngOption [value]="label.value" [label]="label.value"> <span class="example-option-icon material-symbols-outlined">{{label.icon}}</span> <span class="example-option-text">{{label.value}}</span> <span class="example-option-check material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}.select { display: flex; position: relative; align-items: center; color: color-mix(in srgb, var(--hot-pink) 90%, var(--primary-contrast)); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent); border-radius: 0.5rem; border: 1px solid color-mix(in srgb, var(--hot-pink) 80%, transparent);}.select:hover { background-color: color-mix(in srgb, var(--hot-pink) 15%, transparent);}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6; cursor: default;}.selected-label-icon { font-size: 1.25rem;}[ngComboboxInput] { opacity: 0; cursor: pointer; padding: 0 3rem; height: 2.5rem; border: none;}[ngCombobox]:focus-within .select { outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);}.combobox-label { gap: 1rem; left: 1rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 8px; border-radius: 0.5rem; background-color: var(--septenary-contrast); font-size: 0.9rem; max-height: 11rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0.5rem;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px 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);}.example-option-icon { font-size: 1.25rem; padding-right: 1rem;}[ngOption]:not([aria-selected='true']) .example-option-check { display: none;}.example-option-icon,.example-option-check { font-size: 0.9rem;}.example-option-text { flex: 1;}
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The icon that is displayed in the combobox. */ displayIcon = computed(() => { const values = this.listbox()?.values() || []; const label = this.labels.find((label) => label.value === values[0]); return label ? label.icon : ''; }); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = [ {value: 'Important', icon: 'label'}, {value: 'Starred', icon: 'star'}, {value: 'Work', icon: 'work'}, {value: 'Personal', icon: 'person'}, {value: 'To Do', icon: 'checklist'}, {value: 'Later', icon: 'schedule'}, {value: 'Read', icon: 'menu_book'}, {value: 'Travel', icon: 'flight'}, ]; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox class="material-select" readonly> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-icon material-symbols-outlined">{{ displayIcon() }}</span> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label.value) { <div ngOption [value]="label.value" [label]="label.value"> <span class="example-option-icon material-symbols-outlined">{{label.icon}}</span> <span class="example-option-text">{{label.value}}</span> <span class="example-option-check material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@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;}.select { display: flex; position: relative; align-items: center; border-radius: 3rem; color: var(--on-primary); background-color: var(--primary); border: 1px solid color-mix(in srgb, var(--primary) 80%, transparent);}.select:hover { background-color: color-mix(in srgb, var(--primary) 90%, transparent);}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6; cursor: default;}.selected-label-icon { font-size: 1.25rem;}[ngComboboxInput] { opacity: 0; border: none; cursor: pointer; height: 3rem; padding: 0 3rem;}[ngCombobox]:focus-within .select { outline: 2px solid var(--primary); outline-offset: 2px;}.combobox-label { gap: 1rem; left: 1rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 8px; border-radius: 2rem; background-color: var(--septenary-contrast); font-size: 0.9rem; max-height: 13rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; padding: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; padding: 0 1rem; min-height: 3rem; border-radius: 3rem;}[ngOption]:hover,[ngOption][data-active='true'] { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px solid var(--primary);}[ngOption][aria-selected='true'] { color: var(--primary); background-color: color-mix(in srgb, var(--primary) 10%, transparent);}.example-option-icon { font-size: 1.25rem; padding-right: 1rem;}[ngOption]:not([aria-selected='true']) .example-option-check { display: none;}.example-option-icon,.example-option-check { font-size: 0.9rem;}.example-option-text { flex: 1;}
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The icon that is displayed in the combobox. */ displayIcon = computed(() => { const values = this.listbox()?.values() || []; const label = this.labels.find((label) => label.value === values[0]); return label ? label.icon : ''; }); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = [ {value: 'Important', icon: 'label'}, {value: 'Starred', icon: 'star'}, {value: 'Work', icon: 'work'}, {value: 'Personal', icon: 'person'}, {value: 'To Do', icon: 'checklist'}, {value: 'Later', icon: 'schedule'}, {value: 'Read', icon: 'menu_book'}, {value: 'Travel', icon: 'flight'}, ]; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox class="retro-select" readonly> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-icon material-symbols-outlined">{{ displayIcon() }}</span> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label.value) { <div ngOption [value]="label.value" [label]="label.value"> <span class="example-option-icon material-symbols-outlined">{{label.icon}}</span> <span class="example-option-text">{{label.value}}</span> <span class="example-option-check material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@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.8rem; font-family: 'Press Start 2P'; --retro-button-color: color-mix(in srgb, var(--hot-pink) 80%, var(--gray-1000)); --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-elevated-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-flat-shadow: 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-clickable-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 8px 8px 0px 0px var(--gray-700); --retro-pressed-shadow: inset 4px 4px 0px 0px var(--retro-shadow-dark), inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);}.select { display: flex; position: relative; align-items: center; color: var(--page-background); background-color: var(--hot-pink); box-shadow: var(--retro-clickable-shadow);}.select:hover,.select:focus-within { transform: translate(1px, 1px);}.select:active { transform: translate(4px, 4px); box-shadow: var(--retro-pressed-shadow); background-color: color-mix(in srgb, var(--retro-button-color) 60%, var(--gray-50));}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6; cursor: default;}.selected-label-icon { font-size: 1.25rem;}[ngComboboxInput] { opacity: 0; cursor: pointer; padding: 0 4rem; height: 2.5rem; border: none;}.select:has([ngComboboxInput][aria-expanded='false']):focus-within { outline-offset: 8px; outline: 4px dashed var(--hot-pink);}.combobox-label { gap: 1rem; left: 1rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 20px; box-shadow: var(--retro-flat-shadow); background-color: var(--septenary-contrast); max-height: 11rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; padding: 0 1rem; font-size: 0.6rem; min-height: 2.25rem;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px dashed var(--hot-pink);}[ngOption][aria-selected='true'] { color: var(--hot-pink); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);}.example-option-icon { font-size: 1.25rem; padding-right: 1rem;}[ngOption]:not([aria-selected='true']) .example-option-check { display: none;}.example-option-icon,.example-option-check { font-size: 0.9rem;}.example-option-text { flex: 1;}
Usage
The select pattern works best when users need to choose a single value from a familiar set of options.
Consider using this pattern when:
- The option list is fixed (fewer than 20 items) - Users can scan and choose without filtering
- Options are familiar - Users recognize the choices without needing to search
- Forms need standard fields - Country, state, category, or status selection
- Settings and configuration - Dropdown menus for preferences or options
- Clear option labels - Each choice has a distinct, scannable name
Avoid this pattern when:
- The list has more than 20 items - Use the Autocomplete pattern for better filtering
- Users need to search options - Autocomplete provides text input and filtering
- Multiple selection is needed - Use the Multiselect pattern instead
- Very few options exist (2-3) - Radio buttons provide better visibility of all choices
Features
The select pattern combines Combobox and Listbox directives to provide a fully accessible dropdown with:
- Keyboard Navigation - Navigate options with arrow keys, select with Enter, close with Escape
- Screen Reader Support - Built-in ARIA attributes for assistive technologies
- Custom Display - Show selected values with icons, formatting, or rich content
- Signal-Based Reactivity - Reactive state management using Angular signals
- Smart Positioning - CDK Overlay handles viewport edges and scrolling
- Bidirectional Text Support - Automatically handles right-to-left (RTL) languages
Examples
Basic select
Users need a standard dropdown to choose from a list of values. A readonly combobox paired with a listbox provides the familiar select experience with full accessibility support.
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = ['Important', 'Starred', 'Work', 'Personal', 'To Do', 'Later', 'Read', 'Travel']; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox readonly> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label) { <div ngOption [value]="label" [label]="label"> <span class="example-option-text">{{label}}</span> <span class="example-option-check material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}.select { display: flex; position: relative; align-items: center; color: color-mix(in srgb, var(--hot-pink) 90%, var(--primary-contrast)); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent); border-radius: 0.5rem; border: 1px solid color-mix(in srgb, var(--hot-pink) 80%, transparent);}.select:hover { background-color: color-mix(in srgb, var(--hot-pink) 15%, transparent);}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6; cursor: default;}[ngComboboxInput] { opacity: 0; cursor: pointer; padding: 0 1.5rem; height: 2.5rem; border: none;}[ngCombobox]:focus-within .select { outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);}.combobox-label { gap: 1rem; left: 1.5rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 8px; border-radius: 0.5rem; background-color: var(--septenary-contrast); font-size: 0.9rem; max-height: 11rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0.5rem;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px 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.9rem;}.example-option-text { flex: 1;}
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = ['Important', 'Starred', 'Work', 'Personal', 'To Do', 'Later', 'Read', 'Travel']; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox class="material-select" readonly> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label) { <div ngOption [value]="label" [label]="label"> <span class="example-option-text">{{label}}</span> <span class="example-option-check material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@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;}.select { display: flex; position: relative; align-items: center; border-radius: 3rem; color: var(--on-primary); background-color: var(--primary); border: 1px solid color-mix(in srgb, var(--primary) 80%, transparent);}.select:hover { background-color: color-mix(in srgb, var(--primary) 90%, transparent);}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6; cursor: default;}[ngComboboxInput] { opacity: 0; border: none; cursor: pointer; height: 3rem; padding: 0 1.5rem;}[ngCombobox]:focus-within .select { outline: 2px solid var(--primary); outline-offset: 2px;}.combobox-label { gap: 1rem; left: 1.5rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 8px; border-radius: 2rem; background-color: var(--septenary-contrast); font-size: 0.9rem; max-height: 13rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; padding: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; padding: 0 1rem; min-height: 3rem; border-radius: 3rem;}[ngOption]:hover,[ngOption][data-active='true'] { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px 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.9rem;}.example-option-text { flex: 1;}
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = ['Important', 'Starred', 'Work', 'Personal', 'To Do', 'Later', 'Read', 'Travel']; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox class="retro-select" readonly> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label) { <div ngOption [value]="label" [label]="label"> <span class="example-option-text">{{label}}</span> <span class="example-option-check material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@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.8rem; font-family: 'Press Start 2P'; --retro-button-color: color-mix(in srgb, var(--hot-pink) 80%, var(--gray-1000)); --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-elevated-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-flat-shadow: 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-clickable-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 8px 8px 0px 0px var(--gray-700); --retro-pressed-shadow: inset 4px 4px 0px 0px var(--retro-shadow-dark), inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);}.select { display: flex; position: relative; align-items: center; color: var(--page-background); background-color: var(--hot-pink); box-shadow: var(--retro-clickable-shadow);}.select:hover,.select:focus-within { transform: translate(1px, 1px);}.select:active { transform: translate(4px, 4px); box-shadow: var(--retro-pressed-shadow); background-color: color-mix(in srgb, var(--retro-button-color) 60%, var(--gray-50));}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6; cursor: default;}.selected-label-icon { font-size: 1.25rem;}[ngComboboxInput] { opacity: 0; cursor: pointer; padding: 0 3.5rem; height: 2.5rem; border: none;}.select:has([ngComboboxInput][aria-expanded='false']):focus-within { outline-offset: 8px; outline: 4px dashed var(--hot-pink);}.combobox-label { gap: 1rem; left: 1rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 20px; box-shadow: var(--retro-flat-shadow); background-color: var(--septenary-contrast); max-height: 11rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; padding: 0 1rem; font-size: 0.6rem; min-height: 2.25rem;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px dashed var(--hot-pink);}[ngOption][aria-selected='true'] { color: var(--hot-pink); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);}.example-option-icon { font-size: 1.25rem; padding-right: 1rem;}[ngOption]:not([aria-selected='true']) .example-option-check { display: none;}.example-option-icon,.example-option-check { font-size: 0.9rem;}.example-option-text { flex: 1;}
The readonly attribute on ngCombobox prevents text input while preserving keyboard navigation. Users interact with the dropdown using arrow keys and Enter, just like a native select element.
Select with custom display
Options often need visual indicators like icons or badges to help users identify choices quickly. Custom templates within options allow rich formatting while maintaining accessibility.
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The icon that is displayed in the combobox. */ displayIcon = computed(() => { const values = this.listbox()?.values() || []; const label = this.labels.find((label) => label.value === values[0]); return label ? label.icon : ''; }); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = [ {value: 'Important', icon: 'label'}, {value: 'Starred', icon: 'star'}, {value: 'Work', icon: 'work'}, {value: 'Personal', icon: 'person'}, {value: 'To Do', icon: 'checklist'}, {value: 'Later', icon: 'schedule'}, {value: 'Read', icon: 'menu_book'}, {value: 'Travel', icon: 'flight'}, ]; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox readonly> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-icon material-symbols-outlined">{{ displayIcon() }}</span> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label.value) { <div ngOption [value]="label.value" [label]="label.value"> <span class="example-option-icon material-symbols-outlined">{{label.icon}}</span> <span class="example-option-text">{{label.value}}</span> <span class="example-option-check material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}.select { display: flex; position: relative; align-items: center; color: color-mix(in srgb, var(--hot-pink) 90%, var(--primary-contrast)); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent); border-radius: 0.5rem; border: 1px solid color-mix(in srgb, var(--hot-pink) 80%, transparent);}.select:hover { background-color: color-mix(in srgb, var(--hot-pink) 15%, transparent);}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6; cursor: default;}.selected-label-icon { font-size: 1.25rem;}[ngComboboxInput] { opacity: 0; cursor: pointer; padding: 0 3rem; height: 2.5rem; border: none;}[ngCombobox]:focus-within .select { outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);}.combobox-label { gap: 1rem; left: 1rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 8px; border-radius: 0.5rem; background-color: var(--septenary-contrast); font-size: 0.9rem; max-height: 11rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0.5rem;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px 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);}.example-option-icon { font-size: 1.25rem; padding-right: 1rem;}[ngOption]:not([aria-selected='true']) .example-option-check { display: none;}.example-option-icon,.example-option-check { font-size: 0.9rem;}.example-option-text { flex: 1;}
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The icon that is displayed in the combobox. */ displayIcon = computed(() => { const values = this.listbox()?.values() || []; const label = this.labels.find((label) => label.value === values[0]); return label ? label.icon : ''; }); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = [ {value: 'Important', icon: 'label'}, {value: 'Starred', icon: 'star'}, {value: 'Work', icon: 'work'}, {value: 'Personal', icon: 'person'}, {value: 'To Do', icon: 'checklist'}, {value: 'Later', icon: 'schedule'}, {value: 'Read', icon: 'menu_book'}, {value: 'Travel', icon: 'flight'}, ]; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox class="material-select" readonly> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-icon material-symbols-outlined">{{ displayIcon() }}</span> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label.value) { <div ngOption [value]="label.value" [label]="label.value"> <span class="example-option-icon material-symbols-outlined">{{label.icon}}</span> <span class="example-option-text">{{label.value}}</span> <span class="example-option-check material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@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;}.select { display: flex; position: relative; align-items: center; border-radius: 3rem; color: var(--on-primary); background-color: var(--primary); border: 1px solid color-mix(in srgb, var(--primary) 80%, transparent);}.select:hover { background-color: color-mix(in srgb, var(--primary) 90%, transparent);}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6; cursor: default;}.selected-label-icon { font-size: 1.25rem;}[ngComboboxInput] { opacity: 0; border: none; cursor: pointer; height: 3rem; padding: 0 3rem;}[ngCombobox]:focus-within .select { outline: 2px solid var(--primary); outline-offset: 2px;}.combobox-label { gap: 1rem; left: 1rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 8px; border-radius: 2rem; background-color: var(--septenary-contrast); font-size: 0.9rem; max-height: 13rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; padding: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; padding: 0 1rem; min-height: 3rem; border-radius: 3rem;}[ngOption]:hover,[ngOption][data-active='true'] { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px solid var(--primary);}[ngOption][aria-selected='true'] { color: var(--primary); background-color: color-mix(in srgb, var(--primary) 10%, transparent);}.example-option-icon { font-size: 1.25rem; padding-right: 1rem;}[ngOption]:not([aria-selected='true']) .example-option-check { display: none;}.example-option-icon,.example-option-check { font-size: 0.9rem;}.example-option-text { flex: 1;}
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The icon that is displayed in the combobox. */ displayIcon = computed(() => { const values = this.listbox()?.values() || []; const label = this.labels.find((label) => label.value === values[0]); return label ? label.icon : ''; }); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = [ {value: 'Important', icon: 'label'}, {value: 'Starred', icon: 'star'}, {value: 'Work', icon: 'work'}, {value: 'Personal', icon: 'person'}, {value: 'To Do', icon: 'checklist'}, {value: 'Later', icon: 'schedule'}, {value: 'Read', icon: 'menu_book'}, {value: 'Travel', icon: 'flight'}, ]; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox class="retro-select" readonly> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-icon material-symbols-outlined">{{ displayIcon() }}</span> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label.value) { <div ngOption [value]="label.value" [label]="label.value"> <span class="example-option-icon material-symbols-outlined">{{label.icon}}</span> <span class="example-option-text">{{label.value}}</span> <span class="example-option-check material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@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.8rem; font-family: 'Press Start 2P'; --retro-button-color: color-mix(in srgb, var(--hot-pink) 80%, var(--gray-1000)); --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-elevated-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-flat-shadow: 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-clickable-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 8px 8px 0px 0px var(--gray-700); --retro-pressed-shadow: inset 4px 4px 0px 0px var(--retro-shadow-dark), inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);}.select { display: flex; position: relative; align-items: center; color: var(--page-background); background-color: var(--hot-pink); box-shadow: var(--retro-clickable-shadow);}.select:hover,.select:focus-within { transform: translate(1px, 1px);}.select:active { transform: translate(4px, 4px); box-shadow: var(--retro-pressed-shadow); background-color: color-mix(in srgb, var(--retro-button-color) 60%, var(--gray-50));}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6; cursor: default;}.selected-label-icon { font-size: 1.25rem;}[ngComboboxInput] { opacity: 0; cursor: pointer; padding: 0 4rem; height: 2.5rem; border: none;}.select:has([ngComboboxInput][aria-expanded='false']):focus-within { outline-offset: 8px; outline: 4px dashed var(--hot-pink);}.combobox-label { gap: 1rem; left: 1rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 20px; box-shadow: var(--retro-flat-shadow); background-color: var(--septenary-contrast); max-height: 11rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; padding: 0 1rem; font-size: 0.6rem; min-height: 2.25rem;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px dashed var(--hot-pink);}[ngOption][aria-selected='true'] { color: var(--hot-pink); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);}.example-option-icon { font-size: 1.25rem; padding-right: 1rem;}[ngOption]:not([aria-selected='true']) .example-option-check { display: none;}.example-option-icon,.example-option-check { font-size: 0.9rem;}.example-option-text { flex: 1;}
Each option displays an icon alongside the label. The selected value updates to show the chosen option's icon and text, providing clear visual feedback.
Disabled select
Selects can be disabled to prevent user interaction when certain form conditions aren't met. The disabled state provides visual feedback and prevents keyboard interaction.
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = ['Important', 'Starred', 'Work', 'Personal', 'To Do', 'Later', 'Read', 'Travel']; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox readonly disabled> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label) { <div ngOption [value]="label" [label]="label"> <span class="example-option-text">{{label}}</span> <span class="example-option-check material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}.select { display: flex; position: relative; align-items: center; color: color-mix(in srgb, var(--hot-pink) 90%, var(--primary-contrast)); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent); border-radius: 0.5rem; border: 1px solid color-mix(in srgb, var(--hot-pink) 80%, transparent);}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6;}[ngComboboxInput] { opacity: 0; cursor: pointer; padding: 0 1.5rem; height: 2.5rem; border: none;}[ngComboboxInput][aria-disabled='true'] { cursor: default;}[ngCombobox]:focus-within .select { outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);}.combobox-label { gap: 1rem; left: 1.5rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 8px; border-radius: 0.5rem; background-color: var(--septenary-contrast); font-size: 0.9rem; max-height: 11rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0.5rem;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px 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.9rem;}.example-option-text { flex: 1;}
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = ['Important', 'Starred', 'Work', 'Personal', 'To Do', 'Later', 'Read', 'Travel']; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox class="material-select" readonly disabled> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label) { <div ngOption [value]="label" [label]="label"> <span class="example-option-text">{{label}}</span> <span class="example-option-check material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@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;}.select { display: flex; position: relative; align-items: center; border-radius: 3rem; color: var(--on-primary); background-color: var(--primary); border: 1px solid color-mix(in srgb, var(--primary) 80%, transparent);}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6;}[ngComboboxInput] { opacity: 0; border: none; cursor: pointer; height: 3rem; padding: 0 1.5rem;}[ngComboboxInput][aria-disabled='true'] { cursor: default;}[ngCombobox]:focus-within .select { outline: 2px solid var(--primary); outline-offset: 2px;}.combobox-label { gap: 1rem; left: 1.5rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 8px; border-radius: 2rem; background-color: var(--septenary-contrast); font-size: 0.9rem; max-height: 13rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; padding: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; padding: 0 1rem; min-height: 3rem; border-radius: 3rem;}[ngOption]:hover,[ngOption][data-active='true'] { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px 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.9rem;}.example-option-text { flex: 1;}
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = ['Important', 'Starred', 'Work', 'Personal', 'To Do', 'Later', 'Read', 'Travel']; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox class="retro-select" readonly disabled> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label) { <div ngOption [value]="label" [label]="label"> <span class="example-option-text">{{label}}</span> <span class="example-option-check material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@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.8rem; font-family: 'Press Start 2P'; --retro-button-color: color-mix(in srgb, var(--hot-pink) 80%, var(--gray-1000)); --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-elevated-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-flat-shadow: 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-clickable-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 8px 8px 0px 0px var(--gray-700); --retro-pressed-shadow: inset 4px 4px 0px 0px var(--retro-shadow-dark), inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);}.select { display: flex; position: relative; align-items: center; color: var(--page-background); background-color: var(--hot-pink); box-shadow: var(--retro-clickable-shadow);}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6;}.selected-label-icon { font-size: 1.25rem;}[ngComboboxInput] { opacity: 0; cursor: pointer; padding: 0 3.5rem; height: 2.5rem; border: none;}[ngComboboxInput][aria-disabled='true'] { cursor: default;}.select:has([ngComboboxInput][aria-expanded='false']):focus-within { outline-offset: 8px; outline: 4px dashed var(--hot-pink);}.combobox-label { gap: 1rem; left: 1rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 20px; box-shadow: var(--retro-flat-shadow); background-color: var(--septenary-contrast); max-height: 11rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; padding: 0 1rem; font-size: 0.6rem; min-height: 2.25rem;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px dashed var(--hot-pink);}[ngOption][aria-selected='true'] { color: var(--hot-pink); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);}.example-option-icon { font-size: 1.25rem; padding-right: 1rem;}[ngOption]:not([aria-selected='true']) .example-option-check { display: none;}.example-option-icon,.example-option-check { font-size: 0.9rem;}.example-option-text { flex: 1;}
When disabled, the select shows a disabled visual state and blocks all user interaction. Screen readers announce the disabled state to assistive technology users.
APIs
The select pattern uses the following directives from Angular's Aria library. See the full API documentation in the linked guides.
Combobox Directives
The select pattern uses ngCombobox with the readonly attribute to prevent text input while preserving keyboard navigation.
Inputs
| Property | Type | Default | Description |
|---|---|---|---|
readonly |
boolean |
false |
Set to true to create dropdown behavior |
disabled |
boolean |
false |
Disables the entire select |
See the Combobox API documentation for complete details on all available inputs and signals.
Listbox Directives
The select pattern uses ngListbox for the dropdown list and ngOption for each selectable item.
Model
| Property | Type | Description |
|---|---|---|
values |
any[] |
Two-way bindable array of selected values (contains single value for select) |
See the Listbox API documentation for complete details on listbox configuration, selection modes, and option properties.
Positioning
The select pattern integrates with CDK Overlay for smart positioning. Use cdkConnectedOverlay to handle viewport edges and scrolling automatically.