Autocomplete
Overview
An accessible input field that filters and suggests options as users type, helping them find and select values from a list.
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], 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 query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); 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); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="auto-select"> <div #origin class="autocomplete"> <span class="search-icon material-symbols-outlined">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}.autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: var(--quaternary-contrast);}[ngComboboxInput] { width: 13rem; font-size: 1rem; border-radius: 0.25rem; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: var(--primary-contrast); outline-color: var(--hot-pink); border: 1px solid var(--quinary-contrast); background-color: var(--page-background);}[ngComboboxInput]::placeholder { color: var(--quaternary-contrast);}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 8px; padding: 0.5rem; max-height: 11rem; border-radius: 0.5rem; background-color: var(--septenary-contrast); font-size: 0.9rem;}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[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 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']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], 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 query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); 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); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="auto-select"> <div #origin class="material-autocomplete"> <span class="search-icon material-symbols-outlined">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.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);}.material-autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: var(--quaternary-contrast);}[ngComboboxInput] { width: 13rem; font-size: 1rem; border-radius: 3rem; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: var(--primary-contrast); outline-color: var(--primary); border: 1px solid var(--quinary-contrast); background-color: var(--page-background);}[ngComboboxInput]::placeholder { color: var(--quaternary-contrast);}[ngCombobox]:focus-within [ngComboboxInput] { outline: 2px solid var(--primary); outline-offset: 2px;}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 8px; padding: 0.5rem; max-height: 11rem; border-radius: 2rem; background-color: var(--septenary-contrast); font-size: 0.9rem;}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; 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']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], 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 query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); 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); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="auto-select"> <div #origin class="retro-autocomplete"> <span class="search-icon material-symbols-outlined">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.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.6rem; font-family: 'Press Start 2P'; --retro-button-color: #fff; --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-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);}.retro-autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: #000; z-index: 1;}[ngComboboxInput] { width: 15rem; font-size: 0.6rem; border-radius: 0; font-family: 'Press Start 2P'; word-spacing: -5px; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: #000; border: none; box-shadow: var(--retro-flat-shadow); background-color: var(--retro-button-color);}[ngComboboxInput]::placeholder { color: #000; opacity: 0.7;}[ngComboboxInput]:focus { outline: none; transform: translate(1px, 1px); box-shadow: var(--retro-pressed-shadow);}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 20px; padding: 0.5rem; max-height: 11rem; border-radius: 0; background-color: var(--septenary-contrast); box-shadow: var(--retro-flat-shadow);}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0;}[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);}[ngOption]:not([aria-selected='true']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
Usage
Autocomplete works best when users need to select from a large set of options where typing is faster than scrolling. Consider using autocomplete when:
- The option list is long (more than 20 items) - Typing narrows down choices faster than scrolling through a dropdown
- Users know what they're looking for - They can type part of the expected value (like a state name, product, or username)
- Options follow predictable patterns - Users can guess partial matches (like country codes, email domains, or categories)
- Speed matters - Forms benefit from quick selection without extensive navigation
Avoid autocomplete when:
- The list has fewer than 10 options - A regular dropdown or radio group provides better visibility
- Users need to browse options - If discovery is important, show all options upfront
- Options are unfamiliar - Users can't type what they don't know exists in the list
Features
Angular's autocomplete provides a fully accessible combobox implementation with:
- Keyboard Navigation - Navigate options with arrow keys, select with Enter, close with Escape
- Screen Reader Support - Built-in ARIA attributes for assistive technologies
- Three Filter Modes - Choose between auto-select, manual selection, or highlighting behavior
- Signal-Based Reactivity - Reactive state management using Angular signals
- Popover API Integration - Leverages the native HTML Popover API for optimal positioning
- Bidirectional Text Support - Automatically handles right-to-left (RTL) languages
Examples
Auto-select mode
Users typing partial text expect immediate confirmation that their input matches an available option. Auto-select mode updates the input value to match the first filtered option as users type, reducing the number of keystrokes needed and providing instant feedback that their search is on the right track.
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], 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 query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); 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); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="auto-select"> <div #origin class="autocomplete"> <span class="search-icon material-symbols-outlined">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}.autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: var(--quaternary-contrast);}[ngComboboxInput] { width: 13rem; font-size: 1rem; border-radius: 0.25rem; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: var(--primary-contrast); outline-color: var(--hot-pink); border: 1px solid var(--quinary-contrast); background-color: var(--page-background);}[ngComboboxInput]::placeholder { color: var(--quaternary-contrast);}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 8px; padding: 0.5rem; max-height: 11rem; border-radius: 0.5rem; background-color: var(--septenary-contrast); font-size: 0.9rem;}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[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 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']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], 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 query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); 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); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="auto-select"> <div #origin class="material-autocomplete"> <span class="search-icon material-symbols-outlined">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.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);}.material-autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: var(--quaternary-contrast);}[ngComboboxInput] { width: 13rem; font-size: 1rem; border-radius: 3rem; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: var(--primary-contrast); outline-color: var(--primary); border: 1px solid var(--quinary-contrast); background-color: var(--page-background);}[ngComboboxInput]::placeholder { color: var(--quaternary-contrast);}[ngCombobox]:focus-within [ngComboboxInput] { outline: 2px solid var(--primary); outline-offset: 2px;}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 8px; padding: 0.5rem; max-height: 11rem; border-radius: 2rem; background-color: var(--septenary-contrast); font-size: 0.9rem;}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; 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']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], 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 query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); 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); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="auto-select"> <div #origin class="retro-autocomplete"> <span class="search-icon material-symbols-outlined">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.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.6rem; font-family: 'Press Start 2P'; --retro-button-color: #fff; --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-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);}.retro-autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: #000; z-index: 1;}[ngComboboxInput] { width: 15rem; font-size: 0.6rem; border-radius: 0; font-family: 'Press Start 2P'; word-spacing: -5px; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: #000; border: none; box-shadow: var(--retro-flat-shadow); background-color: var(--retro-button-color);}[ngComboboxInput]::placeholder { color: #000; opacity: 0.7;}[ngComboboxInput]:focus { outline: none; transform: translate(1px, 1px); box-shadow: var(--retro-pressed-shadow);}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 20px; padding: 0.5rem; max-height: 11rem; border-radius: 0; background-color: var(--septenary-contrast); box-shadow: var(--retro-flat-shadow);}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0;}[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);}[ngOption]:not([aria-selected='true']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
Manual selection mode
Manual selection mode keeps the typed text unchanged while users navigate the suggestion list, preventing confusion from automatic updates. The input only changes when users explicitly confirm their choice with Enter or a click.
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], 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 query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); 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); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox focusMode="manual"> <div #origin class="autocomplete"> <span class="search-icon material-symbols-outlined">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}.autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: var(--quaternary-contrast);}[ngComboboxInput] { width: 13rem; font-size: 1rem; border-radius: 0.25rem; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: var(--primary-contrast); outline-color: var(--hot-pink); border: 1px solid var(--quinary-contrast); background-color: var(--page-background);}[ngComboboxInput]::placeholder { color: var(--quaternary-contrast);}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 8px; padding: 0.5rem; max-height: 11rem; border-radius: 0.5rem; background-color: var(--septenary-contrast); font-size: 0.9rem;}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[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 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']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], 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 query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); 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); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox focusMode="manual"> <div #origin class="material-autocomplete"> <span class="search-icon material-symbols-outlined">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.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);}.material-autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: var(--quaternary-contrast);}[ngComboboxInput] { width: 13rem; font-size: 1rem; border-radius: 3rem; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: var(--primary-contrast); outline-color: var(--primary); border: 1px solid var(--quinary-contrast); background-color: var(--page-background);}[ngComboboxInput]::placeholder { color: var(--quaternary-contrast);}[ngCombobox]:focus-within [ngComboboxInput] { outline: 2px solid var(--primary); outline-offset: 2px;}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 8px; padding: 0.5rem; max-height: 11rem; border-radius: 2rem; background-color: var(--septenary-contrast); font-size: 0.9rem;}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; 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']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], 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 query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); 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); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox focusMode="manual"> <div #origin class="retro-autocomplete"> <span class="search-icon material-symbols-outlined">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.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.6rem; font-family: 'Press Start 2P'; --retro-button-color: #fff; --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-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);}.retro-autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: #000; z-index: 1;}[ngComboboxInput] { width: 15rem; font-size: 0.6rem; border-radius: 0; font-family: 'Press Start 2P'; word-spacing: -5px; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: #000; border: none; box-shadow: var(--retro-flat-shadow); background-color: var(--retro-button-color);}[ngComboboxInput]::placeholder { color: #000; opacity: 0.7;}[ngComboboxInput]:focus { outline: none; transform: translate(1px, 1px); box-shadow: var(--retro-pressed-shadow);}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 20px; padding: 0.5rem; max-height: 11rem; border-radius: 0; background-color: var(--septenary-contrast); box-shadow: var(--retro-flat-shadow);}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0;}[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);}[ngOption]:not([aria-selected='true']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
Highlight mode
Highlight mode allows the user to navigate options with arrow keys without changing the input value as they browse until they explicitly select a new option with Enter or click.
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], 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 query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); 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); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="highlight"> <div #origin class="autocomplete"> <span class="search-icon material-symbols-outlined">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}.autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: var(--quaternary-contrast);}[ngComboboxInput] { width: 13rem; font-size: 1rem; border-radius: 0.25rem; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: var(--primary-contrast); outline-color: var(--hot-pink); border: 1px solid var(--quinary-contrast); background-color: var(--page-background);}[ngComboboxInput]::placeholder { color: var(--quaternary-contrast);}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 8px; padding: 0.5rem; max-height: 11rem; border-radius: 0.5rem; background-color: var(--septenary-contrast); font-size: 0.9rem;}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[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 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']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], 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 query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); 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); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="highlight"> <div #origin class="material-autocomplete"> <span class="search-icon material-symbols-outlined">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.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);}.material-autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: var(--quaternary-contrast);}[ngComboboxInput] { width: 13rem; font-size: 1rem; border-radius: 3rem; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: var(--primary-contrast); outline-color: var(--primary); border: 1px solid var(--quinary-contrast); background-color: var(--page-background);}[ngComboboxInput]::placeholder { color: var(--quaternary-contrast);}[ngCombobox]:focus-within [ngComboboxInput] { outline: 2px solid var(--primary); outline-offset: 2px;}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 8px; padding: 0.5rem; max-height: 11rem; border-radius: 2rem; background-color: var(--septenary-contrast); font-size: 0.9rem;}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; 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']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], 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 query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); 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); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="highlight"> <div #origin class="retro-autocomplete"> <span class="search-icon material-symbols-outlined">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.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.6rem; font-family: 'Press Start 2P'; --retro-button-color: #fff; --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-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);}.retro-autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: #000; z-index: 1;}[ngComboboxInput] { width: 15rem; font-size: 0.6rem; border-radius: 0; font-family: 'Press Start 2P'; word-spacing: -5px; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: #000; border: none; box-shadow: var(--retro-flat-shadow); background-color: var(--retro-button-color);}[ngComboboxInput]::placeholder { color: #000; opacity: 0.7;}[ngComboboxInput]:focus { outline: none; transform: translate(1px, 1px); box-shadow: var(--retro-pressed-shadow);}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 20px; padding: 0.5rem; max-height: 11rem; border-radius: 0; background-color: var(--septenary-contrast); box-shadow: var(--retro-flat-shadow);}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0;}[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);}[ngOption]:not([aria-selected='true']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
APIs
Combobox Directive
The ngCombobox directive provides the container for autocomplete functionality.
Inputs
| Property | Type | Default | Description |
|---|---|---|---|
filterMode |
'auto-select' | 'manual' | 'highlight' |
'manual' |
Controls selection behavior |
disabled |
boolean |
false |
Disables the combobox |
firstMatch |
string |
- | The value of the first matching item in the popup |
Outputs
| Property | Type | Description |
|---|---|---|
expanded |
Signal<boolean> |
Signal indicating whether the popup is currently open |
ComboboxInput Directive
The ngComboboxInput directive connects an input element to the combobox.
Model
| Property | Type | Description |
|---|---|---|
value |
string |
Two-way bindable string value of the input using [(value)] |
ComboboxPopupContainer Directive
The ngComboboxPopupContainer directive wraps the popup content and manages its display.
Must be used with <ng-template> inside a popover element.
Related components
Autocomplete uses Listbox and Option directives to render the suggestion list. See the Listbox documentation for additional customization options.