Develop robust Angular Autocomplete components that enable dynamic search, dropdown suggestions, and seamless integration with external data sources. The pinnacle Angular Autocomplete solution for contemporary web applications.
Available in Other JavaScript Frameworks
CoreUI Angular Autocomplete Component is also available for Bootstrap, React, and Vue. Explore framework-specific implementations below:
Overview
The CoreUI Angular Autocomplete Component is a powerful, feature-rich autocomplete solution that enhances form usability by providing intelligent suggestions based on user types. Whether you use static data, APIs, or complex search logic, this component delivers a smooth, accessible user experience with extensive customization options.
Key features of this Angular Autocomplete include:
- Dynamic dropdown suggestions with real time filtering
- External data integration with API support
- Advanced search capabilities
- Accessibility-first design
- Custom styles
- Customizable templates
Basic Example
This straightforward demonstration provides a clear guide on how to implement a basic autocomplete input field, emphasizing the essential attributes and configurations required for its functionality.
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { AutocompleteDirective, AutocompleteOption, FormLabelDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete',
imports: [AutocompleteDirective, ReactiveFormsModule, FormLabelDirective],
templateUrl: './autocomplete.component.html'
})
export class AutocompleteComponent {
readonly options: AutocompleteOption[] = ['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js'];
}<label cLabel for="ac-01">Framework</label>
<input
[options]="options"
[searchNoResultsLabel]="'No results found'"
cAutocomplete
cleaner
highlightOptionsOnSearch
indicator
placeholder="Search technologies..."
showHints
type="text"
value="Bootstrap"
id="ac-01"
/>
<div class="form-text">Start typing to search option or provide value</div>You can also use objects with option property for more structured data:
import { Component } from '@angular/core';
import { AutocompleteDirective, AutocompleteOption, FormLabelDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-2',
imports: [AutocompleteDirective, FormLabelDirective],
templateUrl: './autocomplete-2.component.html'
})
export class Autocomplete2Component {
readonly options: AutocompleteOption[] = [
{
label: 'Angular',
value: 1
},
{
label: 'Bootstrap',
value: 2
},
{
label: 'Next.js',
value: 3
},
{
label: 'React.js',
value: 4
},
{
label: 'Vue.js',
value: 5
}
];
}<label cLabel for="ac-02">Framework</label>
<input
[options]="options"
[searchNoResultsLabel]="'No results found'"
cAutocomplete
cleaner
highlightOptionsOnSearch
indicator
placeholder="Search technologies..."
showHints
type="text"
[value]="1"
id="ac-02"
/>
<div class="form-text">Start typing to search option or provide value</div>Each option is identified by its value, or by its label when it has no value, so these must be unique across all options, groups included. Two options with the same identifier cannot be told apart, and in development mode the listbox logs a duplicate option value warning.
For a minimal implementation without additional features:
import { Component } from '@angular/core';
import { AutocompleteDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-3',
imports: [AutocompleteDirective],
templateUrl: './autocomplete-3.component.html'
})
export class Autocomplete3Component {}<input
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
cAutocomplete
/>Search functionality
Configure the search behavior to match your application’s needs. The search prop determines how the component handles user input and filtering.
Default search
By default, search operates only when the input field is focused and filters options internally:
import { Component } from '@angular/core';
import { AutocompleteDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-default-search',
imports: [AutocompleteDirective],
templateUrl: './autocomplete-default-search.component.html'
})
export class AutocompleteDefaultSearchComponent {}<input
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
cAutocomplete
/>Global search
Typing anywhere within the component, for example on the cleaner or indicator button, always continues in the search input, so no configuration is needed. search="global" and { global: true } are deprecated and have no effect.
External search
When external search is enabled search="external", the component delegates search operations to your custom logic or external API. This is perfect for server-side filtering, complex search algorithms, or third-party search services:
<input cAutocomplete
[options]="filteredOptions"
(inputChange)="handleSearch($event)"
search="external"
>See the External Data section for a complete working example.
Restricted selection
Limit users to only select from the provided options by enabling allowOnlyDefinedOptions. This prevents custom value entry:
import { Component } from '@angular/core';
import { AutocompleteDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-restricted-selection',
imports: [AutocompleteDirective],
templateUrl: './autocomplete-restricted-selection.component.html'
})
export class AutocompleteRestrictedSelectionComponent {}<input
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
allowOnlyDefinedOptions
cAutocomplete
/>UX enhancements
Enable intelligent hints and auto-completion features to improve user experience.
Show hints
Display intelligent completion hints that preview the first matching option as user types:
import { Component } from '@angular/core';
import { AutocompleteDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-show-hints',
imports: [AutocompleteDirective],
templateUrl: './autocomplete-show-hints.component.html'
})
export class AutocompleteShowHintsComponent {}<input
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
cAutocomplete
showHints
/>Highlight matching text
Highlight the parts of option labels that match the search with highlightOptionsOnSearch:
import { Component } from '@angular/core';
import { AutocompleteDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-highlight-matching-text',
imports: [AutocompleteDirective],
templateUrl: './autocomplete-highlight-matching-text.component.html'
})
export class AutocompleteHighlightMatchingTextComponent {}<input
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
cAutocomplete
highlightOptionsOnSearch
/>Validation states
Apply validation styling to indicate input validity.
import { Component } from '@angular/core';
import { AutocompleteDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-validation-states',
imports: [AutocompleteDirective],
templateUrl: './autocomplete-validation-states.component.html',
styleUrl: './autocomplete-validation-states.component.scss'
})
export class AutocompleteValidationStatesComponent {}<input
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
[valid]="true"
cAutocomplete
placeholder="Valid autocomplete"
/>
<input
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
[valid]="false"
cAutocomplete
placeholder="Invalid autocomplete"
/>::ng-deep .autocomplete + .autocomplete {
padding-top: .5rem;
}
Disabled state
Disable the component to prevent user interaction:
import { Component } from '@angular/core';
import { AutocompleteDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-disabled-state',
imports: [AutocompleteDirective],
templateUrl: './autocomplete-disabled-state.component.html'
})
export class AutocompleteDisabledStateComponent {}<input
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
cAutocomplete
disabled
indicator
placeholder="Disabled autocomplete..."
/>Read-only state
Make the field read-only with readOnly. Unlike disabled, a read-only field stays focusable, keeps its place in the tab order and submits its form value, but the dropdown does not open and the user cannot change the value by typing, selecting or clearing:
import { Component } from '@angular/core';
import { AutocompleteDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-read-only-state',
imports: [AutocompleteDirective],
templateUrl: './autocomplete-read-only-state.component.html'
})
export class AutocompleteReadOnlyStateComponent {}<input
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
cAutocomplete
cleaner
indicator
readOnly
value="Angular"
/>Sizing
Choose from different sizes to match your design system and form layout:
import { Component } from '@angular/core';
import { AutocompleteDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-sizing',
imports: [AutocompleteDirective],
templateUrl: './autocomplete-sizing.component.html',
styleUrl: './autocomplete-sizing.component.scss'
})
export class AutocompleteSizingComponent {}<input
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
cAutocomplete
placeholder="Large autocomplete..."
sizing="lg"
/>
<input
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
cAutocomplete
placeholder="Default autocomplete..."
/>
<input
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
cAutocomplete
placeholder="Small autocomplete..."
sizing="sm"
/>::ng-deep .autocomplete + .autocomplete {
padding-top: .5rem;
}
Cleaner functionality
Enable a cleaner button to quickly clear input element:
import { Component } from '@angular/core';
import { AutocompleteDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-cleaner-functionality',
imports: [AutocompleteDirective],
templateUrl: './autocomplete-cleaner-functionality.component.html'
})
export class AutocompleteCleanerFunctionalityComponent {}<input
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
cAutocomplete
cleaner
placeholder="With cleaner button..."
/>Dropdown visibility
Bind [(visible)] to open and close the dropdown from your own code. visible is the requested state: the list is shown while the field is enabled, editable and has something to show, so a request made before the options arrive opens the list once they do. Leaving the field, disabling it, or a typed search that matches nothing (without searchNoResultsLabel) cancels the request.
visibleChange reports changes made by the user or by the component itself, such as typing, arrow keys, a click on the field or the indicator, a selection, Escape, a click outside or focus leaving the field, and for visible.set() on the exported directive. It is not emitted for changes made through the visible input, nor on the first render.
import { Component, signal } from '@angular/core';
import { AutocompleteDirective, ButtonDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-dropdown-visibility',
imports: [AutocompleteDirective, ButtonDirective],
templateUrl: './autocomplete-dropdown-visibility.component.html'
})
export class AutocompleteDropdownVisibilityComponent {
readonly visible = signal(false);
}<input
[(visible)]="visible"
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
cAutocomplete
indicator
placeholder="Search technologies..."
/>
<div class="mt-3">
<button (click)="visible.set(!visible())" cButton color="primary">
{{ visible() ? 'Close' : 'Open' }}
</button>
<span class="ms-3">visible: {{ visible() }}</span>
</div>Custom templates
The CoreUI Angular Autocomplete Component provides the flexibility to personalize options and group labels by utilizing custom templates. You can easily customize the options using the optionTemplate, and for groups, you can use optionGroupTemplate, as demonstrated in the examples below:
import { Component, signal } from '@angular/core';
import { AutocompleteDirective, ColComponent, FormLabelDirective, RowComponent } from '@coreui/angular';
import { IconDirective } from '@coreui/icons-angular';
import { cifDe, cifEs, cifGb, cifPl, cifUs } from '@coreui/icons';
@Component({
selector: 'docs-autocomplete-custom-templates',
imports: [AutocompleteDirective, ColComponent, FormLabelDirective, RowComponent, IconDirective],
templateUrl: './autocomplete-custom-templates.component.html'
})
export class AutocompleteCustomTemplatesComponent {
readonly flags: Record<string, string[]> = {
de: cifDe,
es: cifEs,
gb: cifGb,
pl: cifPl,
us: cifUs
};
readonly cities = [
{
label: 'Germany',
value: 'de',
options: [
{
label: 'Saarbrücken'
},
{
label: 'Berlin'
},
{
label: 'München'
}
]
},
{
label: 'Spain',
value: 'es',
options: [
{
label: 'Madrid'
},
{
label: 'Alicante'
},
{
label: 'Huesca'
}
]
},
{
label: 'United Kingdom',
value: 'gb',
options: [
{
label: 'Liverpool'
},
{
label: 'London'
},
{
label: 'Manchester'
}
]
},
{
label: 'United States',
value: 'us',
options: [
{
label: 'Austin'
},
{
label: 'Chicago'
},
{
label: 'Los Angeles'
}
]
}
];
readonly countries = this.cities.map(({ label, value }) => ({ label, value }));
readonly filteredCities = signal(this.cities);
handleOptionChange(country: any) {
if (country === null) {
this.filteredCities.set(this.cities);
return;
}
const match = this.cities.find((c) => c.value === country?.value);
this.filteredCities.set(match ? [match] : this.cities);
}
}<c-row>
<c-col>
<label cLabel for="ac-15-1">Country</label>
<input (optionChange)="handleOptionChange($event)" [optionTemplate]="optionTpl" [options]="countries" cAutocomplete id="ac-15-1" placeholder="Select country" showHints />
</c-col>
<c-col>
<label cLabel for="ac-15-2">City</label>
<input [optionGroupTemplate]="groupTpl" [options]="filteredCities()" cAutocomplete id="ac-15-2" placeholder="Select city" showHints resetSelectionOnOptionsChange />
</c-col>
</c-row>
<ng-template #optionTpl let-idx="idx" let-option>
<div class="d-flex">
<svg [cIcon]="flags[option.value]" class="me-3" size="xl"></svg>
{{ option.label }}
</div>
</ng-template>
<ng-template #groupTpl let-option>
<div class="d-flex align-items-center">
<svg [cIcon]="flags[option.value]" class="me-2" size="lg"></svg>
{{ option.label }}
</div>
</ng-template>External Data
One of the most powerful features of the Angular Autocomplete component is its ability to work with external data sources, such as REST APIs, GraphQL endpoints, or server-side search services. This is essential when dealing with large datasets that shouldn’t be loaded entirely into the client.
Implementation example
Here’s how to implement external data loading with proper debouncing to optimize API calls:
import { JsonPipe } from '@angular/common';
import { HttpErrorResponse } from '@angular/common/http';
import { Component, computed, inject, signal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import {
AutocompleteDirective,
AutocompleteOption,
BadgeComponent,
ButtonDirective,
ColComponent,
FormLabelDirective,
RowComponent
} from '@coreui/angular';
import { UsersService } from './users.service';
import { distinctUntilChanged, map } from 'rxjs/operators';
@Component({
selector: 'docs-autocomplete-implementation',
imports: [
AutocompleteDirective,
BadgeComponent,
FormLabelDirective,
JsonPipe,
ReactiveFormsModule,
ButtonDirective,
RowComponent,
ColComponent
],
templateUrl: './autocomplete-implementation.component.html'
})
export class AutocompleteImplementationComponent {
readonly #usersService = inject(UsersService);
protected formGroup = new FormGroup({
userName: new FormControl<string>('Barbara')
});
readonly searchName = signal<string | undefined>(this.formGroup.value.userName ?? undefined);
#typedName = '';
readonly usersResource = this.#usersService.getUsers(this.searchName);
#users: AutocompleteOption[] = [];
readonly users = computed(() => {
const usersResource = this.usersResource;
if (!usersResource.isLoading()) {
const rawUsers = usersResource?.value()?.records?.map((user) => user.first_name as AutocompleteOption) ?? [];
this.#users = [...new Set(rawUsers)];
}
return this.#users;
});
readonly error = computed(() => this.usersResource?.error() as HttpErrorResponse);
readonly loading = computed(() => this.usersResource?.isLoading());
// readonly #usersEffect = effect(() => {
// console.log('Users:', this.users());
// console.log('Loading:', this.loading());
// console.log('Error:', this.error());
// });
protected handleOptionChange($event: AutocompleteOption | null) {
console.log('* handleOptionChange', $event);
if ($event) {
this.#typedName = '';
this.searchName.set(typeof $event === 'string' ? $event : $event.label);
} else {
this.searchName.set(this.#typedName);
}
}
protected handleValueChange($event: string | number | null | undefined) {
console.log('* handleValueChange', $event);
}
protected handleInputChange($event: string) {
console.log('* handleInputChange', $event);
this.#typedName = $event;
this.searchName.set($event);
}
protected changeValue() {
const findName = 'Markus';
this.searchName.set(findName);
this.formGroup.get('userName')?.setValue(findName);
}
protected resetForm() {
this.#typedName = '';
this.searchName.set(undefined);
this.formGroup.reset();
}
constructor() {
this.formGroup.valueChanges
.pipe(
map((value) => value.userName),
distinctUntilChanged()
)
.subscribe((value) => {
console.log('@ valueChange', value);
});
}
}<form [formGroup]="formGroup">
<c-row>
<c-col>
<label cLabel for="ac-16">Users
<c-badge color="success" size="sm">{{ loading() ? 'loading' : '' }}</c-badge>
</label>
<input
(inputChange)="handleInputChange($event)"
(optionChange)="handleOptionChange($event)"
(valueChange)="handleValueChange($event)"
[clearSearchOnSelect]="false"
[delay]="500"
[loading]="loading()"
[options]="users()"
search="external"
cAutocomplete
cleaner
formControlName="userName"
highlightOptionsOnSearch
id="ac-16"
indicator
placeholder="Search users..."
showHints
virtualScroller
/>
<div class="form-text">Please select the user.</div>
<hr>
<button cButton (click)="changeValue()" class="me-1">Change</button>
<button cButton (click)="resetForm()">Reset</button>
</c-col>
<c-col>
<span> Form value: {{ formGroup.value | json }}</span>
</c-col>
</c-row>
@if (error()) {
@let message = error().error?.message || 'Unknown error';
@let status = error().status;
<div class="text-danger">An error {{ status }} occurred: {{ message }}</div>
}
</form>import { Injectable, Signal } from '@angular/core';
import { HttpParams, httpResource } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class UsersService {
private usersUrl = 'https://apitest.coreui.io/demos/users';
getUsers(userName: Signal<string | undefined>) {
const apiParams: IApiParams = {
limit: 1000,
offset: 0,
sort: 'first_name'
};
return httpResource<IUsersResponse>(() => {
const firstName = userName();
return {
url: this.usersUrl,
params: new HttpParams({ fromObject: { ...apiParams, ...(firstName ? { first_name: firstName } : {}) } })
};
});
}
}
export interface IUsersResponse {
number_of_records: number;
number_of_matching_records: number;
records: IUser[];
}
export interface IUser {
id: number;
first_name: string;
last_name: string;
email: string;
country: string;
ip_address: string;
registered: string;
}
export interface IApiParams {
offset?: number;
limit?: number;
columnFilter?: string;
columnSorter?: string;
sort?: string;
}Resetting the selection
When options come from a server, a selected option may be missing from the next response. Enable resetSelectionOnOptionsChange to clear such a selection: when a new, non-empty options list no longer contains the selected option, and the value matches no other option, the field and the search are emptied, the value becomes '' and optionChange emits null.
<input
(inputChange)="search.set($event)"
[options]="users()"
cAutocomplete
formControlName="user"
resetSelectionOnOptionsChange
search="external"
/>The selection is kept when the new list still contains it, when the list is empty (for example while a request is in flight), when the value is a custom one typed by the user, and when your code writes a new value together with the new options. With external search and the default clearSearchOnSelect, the search is emptied after a selection, so the next list is the one returned for an empty search: a selected option missing from it is cleared.
Performance optimization
Added in v5.7.32For large datasets, enable virtualScroller to render only the options that fit in the dropdown. Only the visible rows and a buffer around them are in the DOM, and keyboard navigation (Arrow Up, Arrow Down, Home, End, Page Up, Page Down) still reaches every option. visibleItems sets the height of the list in rows; itemSize is the initial row height in pixels, replaced by the measured height of a rendered option.
The virtual scroller is loaded on demand, so an application that does not use it does not ship @angular/cdk/scrolling in its initial bundle.
import { Component } from '@angular/core';
import { AutocompleteDirective } from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-virtual-scroller',
imports: [AutocompleteDirective],
templateUrl: './autocomplete-virtual-scroller.component.html'
})
export class AutocompleteVirtualScrollerComponent {
readonly options = Array.from({ length: 10000 }, (_, index) => ({ label: `Option ${index + 1}`, value: index + 1 }));
}<input
[options]="options"
[visibleItems]="8"
cAutocomplete
cleaner
indicator
placeholder="Search 10 000 options..."
virtualScroller
/>Forms
Angular handles user input through reactive, template-driven and signal forms. CoreUI Autocomplete supports all three approaches.
Reactive
The Angular Autocomplete component can be used with reactive forms. You can bind the value to a form control using the formControlName directive.
import { JsonPipe } from '@angular/common';
import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { distinctUntilChanged, map } from 'rxjs/operators';
import {
AutocompleteDirective,
ButtonDirective,
ColComponent,
FormFeedbackComponent,
IAutocompleteOption,
RowComponent
} from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-reactive',
imports: [
AutocompleteDirective,
ButtonDirective,
ColComponent,
FormFeedbackComponent,
JsonPipe,
ReactiveFormsModule,
RowComponent
],
templateUrl: './autocomplete-reactive.component.html'
})
export class AutocompleteReactiveComponent {
readonly formGroup = new FormGroup({
framework: new FormControl<string | null>('React.js', { nonNullable: true, validators: Validators.required })
});
constructor() {
this.formGroup.valueChanges
.pipe(
map((value) => value.framework),
distinctUntilChanged()
)
.subscribe((value) => {
console.log('* Value changed: ', value);
console.log('* Control dirty: ', this.formGroup.get('framework')?.dirty);
console.log('* Control pristine: ', this.formGroup.get('framework')?.pristine);
console.log('* Control touched: ', this.formGroup.get('framework')?.touched);
});
}
resetForm() {
this.formGroup.reset({ framework: '' });
}
protected changeValue() {
this.formGroup.get('framework')?.setValue('Angular');
}
protected setServerError() {
const control = this.formGroup.get('framework');
control?.setErrors({ ...control.errors, server: true });
control?.markAsTouched();
}
protected clearServerError() {
this.formGroup.get('framework')?.updateValueAndValidity();
}
handleOptionChange($event: IAutocompleteOption | null) {
console.log('* handleOptionChange: ', $event);
}
handleValueChange($event: string | number | null | undefined) {
console.log('* handleValueChange: ', $event);
}
handleInputChange($event: any) {
console.log('* handleInputChange: ', `*${$event}*`, typeof $event);
}
}<form [formGroup]="formGroup">
<c-row>
<c-col>
@let control = formGroup.get('framework');
<input
(inputChange)="handleInputChange($event)"
(optionChange)="handleOptionChange($event)"
(valueChange)="handleValueChange($event)"
[options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']"
[valid]="control?.touched ? !control?.invalid : undefined"
cAutocomplete
cleaner
formControlName="framework"
showHints
/>
@let errors = control?.errors;
@if (control?.touched && errors) {
<c-form-feedback [valid]="false">
{{ errors['server'] ? 'framework was rejected by the server' : 'framework is required' }}
</c-form-feedback>
}
</c-col>
<c-col>
<span> Form value: {{ formGroup.value | json }}</span>
<ul>
<li> dirty: {{ formGroup.get('framework')?.dirty }}</li>
<li> pristine: {{ formGroup.get('framework')?.pristine }}</li>
<li> touched: {{ formGroup.get('framework')?.touched }}</li>
<li> errors: {{ formGroup.get('framework')?.errors | json }}</li>
</ul>
</c-col>
</c-row>
</form>
<br>
<button (click)="changeValue()" cButton class="me-1">Change</button>
<button (click)="resetForm()" cButton class="me-1">Reset</button>
<button (click)="setServerError()" cButton class="me-1">Server error</button>
<button (click)="clearServerError()" cButton>Clear server error</button>Template-driven
The Angular Autocomplete component can be used in template-driven forms. Bind the value with [(ngModel)] and read the control state through a template reference to ngModel.
import { JsonPipe } from '@angular/common';
import { Component, signal, viewChild } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import {
AutocompleteDirective,
ButtonDirective,
ColComponent,
FormFeedbackComponent,
IAutocompleteOption,
RowComponent
} from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-template-driven',
imports: [
AutocompleteDirective,
ButtonDirective,
ColComponent,
FormFeedbackComponent,
FormsModule,
JsonPipe,
RowComponent
],
templateUrl: './autocomplete-template-driven.component.html'
})
export class AutocompleteTemplateDrivenComponent {
readonly form = viewChild.required<NgForm>('form');
readonly framework = signal<string | null>('Angular');
readonly frameworks = ['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js'];
protected changeValue() {
this.framework.set('Next.js');
}
protected resetForm() {
this.framework.set('');
this.form().resetForm({ framework: '' });
}
protected handleValueChange($event: number | string | null | undefined) {
console.log('handleValueChange', $event);
}
protected handleOptionChange($event: IAutocompleteOption | null) {
console.log('handleOptionChange', $event);
}
protected handleInputChange($event: string) {
console.log('handleInputChange', $event);
}
}<form #form="ngForm">
<c-row>
<c-col>
<label for="framework22">Framework:</label>
<input
#ctrl="ngModel"
(inputChange)="handleInputChange($event)"
(optionChange)="handleOptionChange($event)"
(valueChange)="handleValueChange($event)"
[(ngModel)]="framework"
[valid]="ctrl.touched ? !ctrl.invalid : undefined"
[delay]="300"
[options]="frameworks"
allowOnlyDefinedOptions
cAutocomplete
cleaner
id="framework22"
name="framework"
required
showHints
/>
@if (ctrl.touched && ctrl.errors?.['required']) {
<c-form-feedback [valid]="false">framework is required</c-form-feedback>
}
</c-col>
<c-col>
<strong>Control state: </strong>
<ul>
<li>Form value: {{ form.value | json }}</li>
<li>Model: {{ framework() | json }}</li>
<li>Dirty: {{ ctrl.dirty }}</li>
<li>Pristine: {{ ctrl.pristine }}</li>
<li>Touched: {{ ctrl.touched }}</li>
<li>Errors: {{ ctrl.errors | json }}</li>
</ul>
</c-col>
</c-row>
<div class="mt-3">
<button type="button" cButton class="me-1" (click)="changeValue()">Change</button>
<button type="button" cButton class="me-1" (click)="resetForm()">Reset</button>
</div>
</form>Signal forms
The Angular Autocomplete component works with signal forms. (preview)
import { JsonPipe } from '@angular/common';
import { Component, signal } from '@angular/core';
import { form, FormField, FormRoot, required } from '@angular/forms/signals';
import {
AutocompleteDirective,
ButtonDirective,
ColComponent,
FormFeedbackComponent,
IAutocompleteOption,
RowComponent
} from '@coreui/angular';
@Component({
selector: 'docs-autocomplete-signal-forms',
imports: [
AutocompleteDirective,
ButtonDirective,
ColComponent,
FormFeedbackComponent,
FormField,
FormRoot,
RowComponent,
JsonPipe
],
templateUrl: './autocomplete-signal-forms.component.html'
})
export class AutocompleteSignalFormsComponent {
readonly frameworkModel = signal({ framework: '' });
readonly frameworkForm = form(this.frameworkModel, (schemaPath) => {
required(schemaPath.framework, { message: 'framework is required' });
});
readonly frameworks = ['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js'];
protected changeValue() {
this.frameworkModel.set({ framework: 'Next.js' });
}
protected resetForm() {
this.frameworkForm().reset({ framework: '' });
}
protected handleValueChange($event: number | string | null | undefined) {
console.log('handleValueChange', $event);
}
protected handleOptionChange($event: IAutocompleteOption | null) {
console.log('handleOptionChange', $event);
}
protected handleInputChange($event: string) {
console.log('handleInputChange', $event);
}
}<form [formRoot]="frameworkForm">
<c-row>
<c-col>
<label for="framework21">Framework:</label>
@let ctrlState = frameworkForm.framework();
<input
(inputChange)="handleInputChange($event)"
(optionChange)="handleOptionChange($event)"
(valueChange)="handleValueChange($event)"
[delay]="300"
[formField]="frameworkForm.framework"
[options]="frameworks"
[valid]="
ctrlState.touched() && ctrlState.invalid()
? false
: ctrlState.touched() && !ctrlState.invalid()
? true
: undefined
"
allowOnlyDefinedOptions
cAutocomplete
cleaner
id="framework21"
showHints
/>
@if (ctrlState.touched() && ctrlState.invalid()) {
@for (error of ctrlState.errors(); track error.kind) {
@if (error.kind === 'required') {
<c-form-feedback [valid]="false">
{{ error.message }}
</c-form-feedback>
}
}
}
</c-col>
<c-col>
<strong>Control state: </strong>
<ul>
<li>Value: {{ frameworkForm.framework().value() | json }}</li>
<li>Dirty: {{ frameworkForm.framework().dirty() }}</li>
<li>Touched: {{ frameworkForm.framework().touched() }}</li>
<li>Errors: {{ frameworkForm.framework().errors() | json }}</li>
</ul>
</c-col>
</c-row>
<div class="mt-3">
<button type="button" cButton class="me-1" (click)="changeValue()">Change</button>
<button type="reset" cButton class="me-1" (click)="resetForm()">Reset</button>
</div>
</form>Accessibility
The Autocomplete component includes several accessibility features:
- ARIA attributes: The input is a
comboboxthat controls alistboxofoptionelements, witharia-expanded,aria-controls,aria-haspopup,aria-autocompleteandaria-selectedkept in sync. Built on@angular/aria. - Screen reader support: Descriptive labels and announcements for state changes
- Keyboard navigation: Full keyboard support with arrow keys, Home, End, Page Up, Page Down, Enter, Escape, and Tab
- Focus management: Focus stays in the input while navigating; the highlighted option is announced through
aria-activedescendant - Option groups: Grouped options are rendered inside a
groupnamed by its label, so screen readers announce the group of the highlighted option. WithvirtualScroller, each option references its group labels througharia-describedbyinstead. - Semantic markup: Uses appropriate HTML elements and structure
Keyboard shortcuts
| Key | Action |
|---|---|
| Arrow Down | Navigate to the next option or open dropdown |
| Arrow Up | Navigate to the previous option |
| Page Down Page Up | Move the highlight by visibleItems options |
| Home End | Move the highlight to the first or last option while the dropdown is open |
| Enter | Select the highlighted option, or the typed text when no option is highlighted (with allowOnlyDefinedOptions only text matching an option label is accepted) |
| Escape | Close the dropdown |
| Tab | Accept hint completion (when hints are enabled), or select the highlighted option and move focus |
| Backspace Delete | Clear input and trigger search |
Customizing
CSS variables
Angular CoreUI Autocomplete use local CSS variables for easy customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too.
.autocomplete {
--cui-autocomplete-zindex: #{$autocomplete-zindex};
--cui-autocomplete-font-family: #{$autocomplete-font-family};
--cui-autocomplete-font-size: #{$autocomplete-font-size};
--cui-autocomplete-font-weight: #{$autocomplete-font-weight};
--cui-autocomplete-line-height: #{$autocomplete-line-height};
--cui-autocomplete-color: #{$autocomplete-color};
--cui-autocomplete-bg: #{$autocomplete-bg};
--cui-autocomplete-box-shadow: #{$autocomplete-box-shadow};
--cui-autocomplete-border-width: #{$autocomplete-border-width};
--cui-autocomplete-border-color: #{$autocomplete-border-color};
--cui-autocomplete-border-radius: #{$autocomplete-border-radius};
--cui-autocomplete-disabled-color: #{$autocomplete-disabled-color};
--cui-autocomplete-disabled-bg: #{$autocomplete-disabled-bg};
--cui-autocomplete-disabled-border-color: #{$autocomplete-disabled-border-color};
--cui-autocomplete-focus-color: #{$autocomplete-focus-color};
--cui-autocomplete-focus-bg: #{$autocomplete-focus-bg};
--cui-autocomplete-focus-border-color: #{$autocomplete-focus-border-color};
--cui-autocomplete-focus-box-shadow: #{$autocomplete-focus-box-shadow};
--cui-autocomplete-placeholder-color: #{$autocomplete-placeholder-color};
--cui-autocomplete-padding-y: #{$autocomplete-padding-y};
--cui-autocomplete-padding-x: #{$autocomplete-padding-x};
--cui-autocomplete-cleaner-width: #{$autocomplete-cleaner-width};
--cui-autocomplete-cleaner-height: #{$autocomplete-cleaner-height};
--cui-autocomplete-cleaner-padding-y: #{$autocomplete-cleaner-padding-y};
--cui-autocomplete-cleaner-padding-x: #{$autocomplete-cleaner-padding-x};
--cui-autocomplete-cleaner-icon: #{escape-svg($autocomplete-cleaner-icon)};
--cui-autocomplete-cleaner-icon-color: #{$autocomplete-cleaner-icon-color};
--cui-autocomplete-cleaner-icon-hover-color: #{$autocomplete-cleaner-icon-hover-color};
--cui-autocomplete-cleaner-icon-size: #{$autocomplete-cleaner-icon-size};
--cui-autocomplete-indicator-width: #{$autocomplete-indicator-width};
--cui-autocomplete-indicator-height: #{$autocomplete-indicator-height};
--cui-autocomplete-indicator-padding-y: #{$autocomplete-indicator-padding-y};
--cui-autocomplete-indicator-padding-x: #{$autocomplete-indicator-padding-x};
--cui-autocomplete-indicator-icon: #{escape-svg($autocomplete-indicator-icon)};
--cui-autocomplete-indicator-icon-color: #{$autocomplete-indicator-icon-color};
--cui-autocomplete-indicator-icon-hover-color: #{$autocomplete-indicator-icon-hover-color};
--cui-autocomplete-indicator-icon-size: #{$autocomplete-indicator-icon-size};
--cui-autocomplete-dropdown-min-width: #{$autocomplete-dropdown-min-width};
--cui-autocomplete-dropdown-bg: #{$autocomplete-dropdown-bg};
--cui-autocomplete-dropdown-border-width: #{$autocomplete-dropdown-border-width};
--cui-autocomplete-dropdown-border-color: #{$autocomplete-dropdown-border-color};
--cui-autocomplete-dropdown-border-radius: #{$autocomplete-dropdown-border-radius};
--cui-autocomplete-dropdown-box-shadow: #{$autocomplete-dropdown-box-shadow};
--cui-autocomplete-options-padding-y: #{$autocomplete-options-padding-y};
--cui-autocomplete-options-padding-x: #{$autocomplete-options-padding-x};
--cui-autocomplete-options-font-size: #{$autocomplete-options-font-size};
--cui-autocomplete-options-font-weight: #{$autocomplete-options-font-weight};
--cui-autocomplete-options-color: #{$autocomplete-options-color};
--cui-autocomplete-optgroup-label-padding-y: #{$autocomplete-optgroup-label-padding-y};
--cui-autocomplete-optgroup-label-padding-x: #{$autocomplete-optgroup-label-padding-x};
--cui-autocomplete-optgroup-label-font-size: #{$autocomplete-optgroup-label-font-size};
--cui-autocomplete-optgroup-label-font-weight: #{$autocomplete-optgroup-label-font-weight};
--cui-autocomplete-optgroup-label-color: #{$autocomplete-optgroup-label-color};
--cui-autocomplete-optgroup-label-text-transform: #{$autocomplete-optgroup-label-text-transform};
--cui-autocomplete-option-padding-y: #{$autocomplete-option-padding-y};
--cui-autocomplete-option-padding-x: #{$autocomplete-option-padding-x};
--cui-autocomplete-option-margin-y: #{$autocomplete-option-margin-y};
--cui-autocomplete-option-margin-x: #{$autocomplete-option-margin-x};
--cui-autocomplete-option-border-width: #{$autocomplete-option-border-width};
--cui-autocomplete-option-border-color: #{$autocomplete-option-border-color};
--cui-autocomplete-option-border-radius: #{$autocomplete-option-border-radius};
--cui-autocomplete-option-box-shadow: #{$autocomplete-option-box-shadow};
--cui-autocomplete-option-hover-color: #{$autocomplete-option-hover-color};
--cui-autocomplete-option-hover-bg: #{$autocomplete-option-hover-bg};
--cui-autocomplete-option-focus-box-shadow: #{$autocomplete-option-focus-box-shadow};
--cui-autocomplete-option-disabled-color: #{$autocomplete-option-disabled-color};
--cui-autocomplete-option-indicator-width: #{$autocomplete-option-indicator-width};
--cui-autocomplete-option-indicator-bg: #{$autocomplete-option-indicator-bg};
--cui-autocomplete-option-indicator-border: #{$autocomplete-option-indicator-border};
--cui-autocomplete-option-indicator-border-radius: #{$autocomplete-option-indicator-border-radius};
--cui-autocomplete-option-selected-bg: #{$autocomplete-option-selected-bg};
--cui-autocomplete-option-selected-indicator-bg: #{$autocomplete-option-selected-indicator-bg};
--cui-autocomplete-option-selected-indicator-bg-image: #{escape-svg($autocomplete-option-selected-indicator-bg-image)};
--cui-autocomplete-option-selected-indicator-border-color: #{$autocomplete-option-selected-indicator-border-color};
}SASS variables
$autocomplete-zindex: 1000 !default;
$autocomplete-font-family: $input-font-family !default;
$autocomplete-font-size: $input-font-size !default;
$autocomplete-font-weight: $input-font-weight !default;
$autocomplete-line-height: $input-line-height !default;
$autocomplete-padding-y: $input-padding-y !default;
$autocomplete-padding-x: $input-padding-x !default;
$autocomplete-color: $input-color !default;
$autocomplete-bg: $input-bg !default;
$autocomplete-box-shadow: $box-shadow-inset !default;
$autocomplete-border-width: $input-border-width !default;
$autocomplete-border-color: $input-border-color !default;
$autocomplete-border-radius: $input-border-radius !default;
$autocomplete-border-radius-sm: $input-border-radius-sm !default;
$autocomplete-border-radius-lg: $input-border-radius-lg !default;
$autocomplete-disabled-color: $input-disabled-color !default;
$autocomplete-disabled-bg: $input-disabled-bg !default;
$autocomplete-disabled-border-color: $input-disabled-border-color !default;
$autocomplete-focus-color: $input-focus-color !default;
$autocomplete-focus-bg: $input-focus-bg !default;
$autocomplete-focus-border-color: $input-focus-border-color !default;
$autocomplete-focus-box-shadow: $input-btn-focus-box-shadow !default;
$autocomplete-placeholder-color: var(--cui-secondary-color) !default;
$autocomplete-invalid-border-color: $form-invalid-border-color !default;
$autocomplete-valid-border-color: $form-valid-border-color !default;
$autocomplete-cleaner-width: 1.5rem !default;
$autocomplete-cleaner-height: 1.5rem !default;
$autocomplete-cleaner-padding-x: 0 !default;
$autocomplete-cleaner-padding-y: 0 !default;
$autocomplete-cleaner-icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#000'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>") !default;
$autocomplete-cleaner-icon-color: var(--cui-tertiary-color) !default;
$autocomplete-cleaner-icon-hover-color: var(--cui-body-color) !default;
$autocomplete-cleaner-icon-size: .625rem !default;
$autocomplete-indicator-width: 1.5rem !default;
$autocomplete-indicator-height: 1.5rem !default;
$autocomplete-indicator-padding-x: 0 !default;
$autocomplete-indicator-padding-y: 0 !default;
$autocomplete-indicator-icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' fill='#000'><path d='M256.045 416.136.717 160.807l29.579-29.579 225.749 225.748 225.749-225.748 29.579 29.579-255.328 255.329z'/></svg>") !default;
$autocomplete-indicator-icon-color: var(--cui-tertiary-color) !default;
$autocomplete-indicator-icon-hover-color: var(--cui-body-color) !default;
$autocomplete-indicator-icon-size: .75rem !default;
$autocomplete-dropdown-min-width: 100% !default;
$autocomplete-dropdown-bg: var(--cui-body-bg) !default;
$autocomplete-dropdown-border-color: var(--cui-border-color) !default;
$autocomplete-dropdown-border-width: var(--cui-border-width) !default;
$autocomplete-dropdown-border-radius: var(--cui-border-radius) !default;
$autocomplete-dropdown-box-shadow: var(--cui-box-shadow) !default;
$autocomplete-options-padding-y: .5rem !default;
$autocomplete-options-padding-x: .5rem !default;
$autocomplete-options-font-size: $font-size-base !default;
$autocomplete-options-font-weight: $font-weight-normal !default;
$autocomplete-options-color: var(--cui-body-color) !default;
$autocomplete-optgroup-label-padding-y: .5rem !default;
$autocomplete-optgroup-label-padding-x: .625rem !default;
$autocomplete-optgroup-label-font-size: 80% !default;
$autocomplete-optgroup-label-font-weight: $font-weight-bold !default;
$autocomplete-optgroup-label-color: var(--cui-tertiary-color) !default;
$autocomplete-optgroup-label-text-transform: uppercase !default;
$autocomplete-option-padding-y: .5rem !default;
$autocomplete-option-padding-x: .75rem !default;
$autocomplete-option-margin-y: 1px !default;
$autocomplete-option-margin-x: 0 !default;
$autocomplete-option-border-width: $input-border-width !default;
$autocomplete-option-border-color: transparent !default;
$autocomplete-option-border-radius: var(--cui-border-radius) !default;
$autocomplete-option-box-shadow: $box-shadow-inset !default;
$autocomplete-option-hover-color: var(--cui-body-color) !default;
$autocomplete-option-hover-bg: var(--cui-tertiary-bg) !default;
$autocomplete-option-focus-box-shadow: $input-btn-focus-box-shadow !default;
$autocomplete-option-indicator-width: 1em !default;
$autocomplete-option-indicator-bg: $form-check-input-bg !default;
$autocomplete-option-indicator-border: $form-check-input-border !default;
$autocomplete-option-indicator-border-radius: .25em !default;
$autocomplete-option-selected-bg: var(--cui-secondary-bg) !default;
$autocomplete-option-selected-indicator-bg: $form-check-input-checked-bg-color !default;
$autocomplete-option-selected-indicator-bg-image: $form-check-input-checked-bg-image !default;
$autocomplete-option-selected-indicator-border-color: $autocomplete-option-selected-indicator-bg !default;
$autocomplete-option-disabled-color: var(--cui-secondary-color) !default;
$autocomplete-font-size-lg: $input-font-size-lg !default;
$autocomplete-padding-y-lg: $input-padding-y-lg !default;
$autocomplete-padding-x-lg: $input-padding-x-lg !default;
$autocomplete-font-size-sm: $input-font-size-sm !default;
$autocomplete-padding-y-sm: $input-padding-y-sm !default;
$autocomplete-padding-x-sm: $input-padding-x-sm !default;API reference
Autocomplete Module
import { NgModule } from '@angular/core';
import { AutocompleteModule } from '@coreui/angular';
@NgModule({
imports: [AutocompleteModule]
})
export class CustomAppModule {}Autocomplete Standalone
import { Component } from '@angular/core';
import { AutocompleteDirective } from '@coreui/angular';
@Component({
template: ` <input [options]="['Angular', 'Bootstrap', 'Next.js', 'React.js', 'Vue.js']" cAutocomplete /> `,
imports: [AutocompleteDirective],
standalone: true
})
export class CustomAppComponent {}cAutocomplete
directive
import { AutocompleteDirective } from '@coreui/angular-pro'