Angular Chip Input component lets users enter multiple values as chips in one field, with keyboard entry, removal, selection and Angular forms support.
Available in Other JavaScript Frameworks
CoreUI Angular Chip Input Component is also available for Bootstrap, React, and Vue. Explore framework-specific implementations below:
Overview
The CoreUI Angular Chip Input component lets users enter multiple values as chips inside a single field. Use it as a tag input, multi-value selector, or token field for skills, categories, email recipients, and more. It supports keyboard-friendly entry, chip removal, and optional selection.
- Type values and press
Enteror the separator to create chips; pasted text is split the same way. - Chips can be removable and selectable.
- The text field stays inline and grows as you type.
- The value is a
string[]: bind[(value)], or useformControlName,ngModelor[formField].
When to use Chip Input
Use the Angular Chip Input component when you need:
- A tag input or token field for free-form multi-value entry
- An email or recipient input where users add multiple addresses
- A skills or category selector in forms
- A multi-value field that works with Angular forms
Basic example
Use c-chip-input to render a multi-value field with predefined chips and an inline text field. label renders an inline label inside the container; the id goes to the text field, so the label, or an external <label for>, names it.
import { Component } from '@angular/core';
import { ChipInputComponent } from '@coreui/angular';
@Component({
selector: 'docs-chip-input-basic',
templateUrl: './chip-input-basic.component.html',
imports: [ChipInputComponent]
})
export class ChipInputBasicComponent {}<c-chip-input
id="skillsInputBasic"
label="Skills:"
placeholder="Add a skill..."
[value]="['JavaScript', 'TypeScript', 'Accessibility']"
/>Variants
Use chipClassName to give chips contextual classes that represent categories, status, or priority.
import { Component } from '@angular/core';
import { ChipInputComponent } from '@coreui/angular';
@Component({
selector: 'docs-chip-input-variants',
templateUrl: './chip-input-variants.component.html',
imports: [ChipInputComponent]
})
export class ChipInputVariantsComponent {
readonly variants: Record<string, string> = {
approved: 'chip-success',
blocking: 'chip-danger',
feature: 'chip-primary',
'needs review': 'chip-warning'
};
variant = (value: string) => this.variants[value.trim().toLowerCase()] ?? '';
}<c-chip-input
aria-label="Labels"
placeholder="Add label..."
[chipClassName]="variant"
[value]="['Feature', 'Approved', 'Needs review', 'Blocking']"
/>In the example below, the chip color is assigned from the chip text by a chipClassName function, with a fallback class for any other value.
import { Component } from '@angular/core';
import { ChipInputComponent } from '@coreui/angular';
@Component({
selector: 'docs-chip-input-class-name',
templateUrl: './chip-input-class-name.component.html',
imports: [ChipInputComponent]
})
export class ChipInputClassNameComponent {
variant = (value: string) => {
const variants: Record<string, string> = {
approved: 'chip-success',
blocking: 'chip-danger',
feature: 'chip-primary'
};
return variants[value.trim().toLowerCase()] ?? 'chip-secondary';
};
}<c-chip-input
aria-label="Labels"
placeholder="Type and press Enter"
[chipClassName]="variant"
[value]="['Feature', 'Blocking', 'Approved']"
/>Sizes
Use size="sm" and size="lg" to match surrounding form controls. The default size has no size modifier.
import { Component } from '@angular/core';
import { ChipInputComponent } from '@coreui/angular';
@Component({
selector: 'docs-chip-input-sizes',
templateUrl: './chip-input-sizes.component.html',
imports: [ChipInputComponent]
})
export class ChipInputSizesComponent {}<div class="mb-3">
<c-chip-input id="skillsInputSm" label="Small" size="sm" placeholder="Add small tag..." [value]="['HTML']" />
</div>
<div class="mb-3">
<c-chip-input id="skillsInputMd" label="Default" placeholder="Add default tag..." [value]="['JavaScript']" />
</div>
<div>
<c-chip-input id="skillsInputLg" label="Large" size="lg" placeholder="Add large tag..." [value]="['TypeScript']" />
</div>Empty state
Start with just the text field and let users add chips as they type.
import { Component } from '@angular/core';
import { ChipInputComponent } from '@coreui/angular';
@Component({
selector: 'docs-chip-input-empty-state',
templateUrl: './chip-input-empty-state.component.html',
imports: [ChipInputComponent]
})
export class ChipInputEmptyStateComponent {}<c-chip-input aria-label="Tags" placeholder="Type and press Enter" />With label
Use a standard form label for accessibility. Without a visible label, give the component an aria-label. aria-label, aria-labelledby and aria-describedby set on c-chip-input move to its text field, so a hint or a c-form-feedback can describe it; bind them as [aria-describedby], not [attr.aria-describedby], which does not reach the text field.
import { Component } from '@angular/core';
import { ChipInputComponent, FormLabelDirective, FormTextDirective } from '@coreui/angular';
@Component({
selector: 'docs-chip-input-with-label',
templateUrl: './chip-input-with-label.component.html',
imports: [ChipInputComponent, FormLabelDirective, FormTextDirective]
})
export class ChipInputWithLabelComponent {}<div class="mb-3">
<label cLabel for="techStackInput">Tech stack</label>
<c-chip-input
id="techStackInput"
aria-describedby="techStackHint"
placeholder="Add package..."
[value]="['Angular', 'Vite']"
/>
<div id="techStackHint" cFormText>Press Enter or comma to add a value.</div>
</div>Disabled
Set disabled to make the field and the chips non-interactive; the remove buttons are hidden.
import { Component } from '@angular/core';
import { ChipInputComponent } from '@coreui/angular';
@Component({
selector: 'docs-chip-input-disabled',
templateUrl: './chip-input-disabled.component.html',
imports: [ChipInputComponent]
})
export class ChipInputDisabledComponent {}<c-chip-input aria-label="Locked tags" disabled placeholder="Input disabled" [value]="['Read only', 'Locked']" />Readonly
Use readonly when chips should stay visible, but values must not change: chips cannot be added, removed or toggled.
import { Component } from '@angular/core';
import { ChipInputComponent } from '@coreui/angular';
@Component({
selector: 'docs-chip-input-readonly',
templateUrl: './chip-input-readonly.component.html',
imports: [ChipInputComponent]
})
export class ChipInputReadonlyComponent {}<c-chip-input aria-label="Languages" readonly placeholder="Read-only values" [value]="['JavaScript', 'TypeScript']" />Selectable chips
Enable selection for the chips managed by Chip Input and bind [(selected)] to the selected values. Focusing the text field clears the selection, so Tab from the chips into the field clears it too.
import { Component, signal } from '@angular/core';
import { ChipInputComponent } from '@coreui/angular';
@Component({
selector: 'docs-chip-input-selectable',
templateUrl: './chip-input-selectable.component.html',
imports: [ChipInputComponent]
})
export class ChipInputSelectableComponent {
readonly selected = signal<string[]>([]);
}<c-chip-input
aria-label="Teams"
selectable
placeholder="Select chips"
[value]="['Design', 'Backend', 'QA', 'DevOps']"
[(selected)]="selected"
/>
<p class="small text-body-secondary mt-2 mb-0">Selected: {{ selected().join(', ') || 'None' }}</p>Forms
Chip Input works with reactive forms, template-driven forms and signal forms without a value accessor; the form value is a string[]. An error is shown once the control has been touched, that is after focus leaves the component, and validationState set by the page wins over the form. Place the message in a c-form-feedback right after the component and point aria-describedby at it. The field frame does not change color: the state is carried by the message and by aria-invalid on the text field.
Reactive
import { JsonPipe } from '@angular/common';
import { Component } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ChipInputComponent, FormFeedbackComponent, FormLabelDirective } from '@coreui/angular';
@Component({
selector: 'docs-chip-input-reactive',
templateUrl: './chip-input-reactive.component.html',
imports: [ChipInputComponent, FormFeedbackComponent, FormLabelDirective, JsonPipe, ReactiveFormsModule]
})
export class ChipInputReactiveComponent {
readonly form = new FormGroup({
tags: new FormControl<string[]>(['Angular'], Validators.required)
});
readonly formValue = toSignal(this.form.valueChanges, { initialValue: this.form.value });
}<form [formGroup]="form">
<label cLabel for="tagsReactive">Tags</label>
<c-chip-input
id="tagsReactive"
aria-describedby="tagsReactiveFeedback"
formControlName="tags"
placeholder="Add a tag..."
/>
<c-form-feedback id="tagsReactiveFeedback" [valid]="false">Add at least one tag.</c-form-feedback>
</form>
<p class="small text-body-secondary mt-2 mb-0">Form value: {{ formValue() | json }}</p>Template driven
Validator directives next to ngModel apply to the chips: required rejects an empty list and minlength sets the smallest number of chips.
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ChipInputComponent, FormFeedbackComponent, FormLabelDirective } from '@coreui/angular';
@Component({
selector: 'docs-chip-input-template-driven',
templateUrl: './chip-input-template-driven.component.html',
imports: [ChipInputComponent, FormFeedbackComponent, FormLabelDirective, FormsModule]
})
export class ChipInputTemplateDrivenComponent {
tags: string[] = ['Angular'];
}<label cLabel for="tagsTemplate">Tags</label>
<c-chip-input
id="tagsTemplate"
aria-describedby="tagsTemplateFeedback"
placeholder="Add a tag..."
required
[(ngModel)]="tags"
/>
<c-form-feedback id="tagsTemplateFeedback" [valid]="false">Add at least one tag.</c-form-feedback>
<p class="small text-body-secondary mt-2 mb-0">Tags: {{ tags.join(', ') || 'none' }}</p>Signal forms
required() in signal forms does not treat an empty array as empty, so add minLength(path, 1) to reject a field with no chips; required() still adds aria-required to the text field.
import { Component, signal } from '@angular/core';
import { form, FormField, minLength, required } from '@angular/forms/signals';
import { ChipInputComponent, FormFeedbackComponent, FormLabelDirective } from '@coreui/angular';
@Component({
selector: 'docs-chip-input-signal-forms',
templateUrl: './chip-input-signal-forms.component.html',
imports: [ChipInputComponent, FormFeedbackComponent, FormField, FormLabelDirective]
})
export class ChipInputSignalFormsComponent {
readonly model = signal({ tags: ['Angular'] });
readonly tagsForm = form(this.model, (path) => {
required(path.tags);
minLength(path.tags, 1, { message: 'Add at least one tag.' });
});
}<label cLabel for="tagsSignal">Tags</label>
<c-chip-input
id="tagsSignal"
aria-describedby="tagsSignalFeedback"
placeholder="Add a tag..."
[formField]="tagsForm.tags"
/>
<c-form-feedback id="tagsSignalFeedback" [valid]="false">
@for (error of tagsForm.tags().errors(); track error) {
{{ error.message }}
}
</c-form-feedback>
<p class="small text-body-secondary mt-2 mb-0">Tags: {{ model().tags.join(', ') || 'none' }}</p>Keyboard behavior
When the text field is focused
| Key | Action |
|---|---|
Enter | Create chips from the text |
, (or separator) | Create a chip from the text before it |
Backspace / Delete | When the field is empty, move focus to the last chip |
ArrowLeft | When the caret is at the start, move focus to the last chip (ArrowRight in right-to-left layouts) |
Escape | Clear the text and leave the field |
When a chip is focused
| Key | Action |
|---|---|
Enter / Space | Toggle selection (selectable chips) |
Backspace / Delete | Remove the chip (removable chips) |
ArrowLeft / ArrowRight | Move to the previous / next chip; ArrowRight on the last chip returns to the field (mirrored in right-to-left layouts) |
Home / End | Move to the first / last chip |
| Any character | Move focus to the text field and type it |
Accessibility
- Name the text field with
label, an external<label for>pointing at itsid, oraria-label, which moves to the field likearia-describedby. - Focusable chips and the text field are two tab stops: the last chip and the field.
Shift+Tabfrom the field reaches the last chip; the arrow keys move between chips. - Remove buttons are named after their chip, e.g. “Remove Angular”;
ariaRemoveLabelsets the prefix. - Selectable chips are toggle buttons with
aria-pressed. In areadonlycontrol they are markedaria-disabled. - When the focused chip is removed, focus moves to a neighboring chip, or to the text field when no chip is left.
- Added and removed chips are announced with their value through a page live region, one message per update: “Angular added”, “Angular removed”. Change the wording with
ariaAddedAnnouncementandariaRemovedAnnouncement. - With a form,
aria-requiredandaria-invalidare set on the text field.
Customizing
CSS variables
Angular chip inputs use local CSS variables on .chip-input for enhanced real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too.
--cui-chip-input-min-height: #{$chip-input-min-height};
--cui-chip-input-padding-y: #{$chip-input-padding-y};
--cui-chip-input-padding-x: #{$chip-input-padding-x};
--cui-chip-input-font-size: #{$chip-input-font-size};
--cui-chip-input-bg: #{$chip-input-bg};
--cui-chip-input-color: #{$chip-input-color};
--cui-chip-input-border-width: #{$chip-input-border-width};
--cui-chip-input-border-color: #{$chip-input-border-color};
--cui-chip-input-border-radius: #{$chip-input-border-radius};
--cui-chip-input-gap: #{$chip-input-gap};
--cui-chip-input-transition: #{$chip-input-transition};Sass variables
$chip-input-min-height: $input-height !default;
$chip-input-padding-y: .25rem !default;
$chip-input-padding-x: .75rem !default;
$chip-input-font-size: $input-font-size !default;
$chip-input-bg: var(--cui-body-bg) !default;
$chip-input-color: var(--cui-body-color) !default;
$chip-input-border-width: var(--cui-border-width) !default;
$chip-input-border-color: var(--cui-border-color) !default;
$chip-input-border-radius: var(--cui-border-radius) !default;
$chip-input-gap: .375rem !default;
$chip-input-transition: $input-transition !default;
$chip-input-min-height-sm: $input-height-sm !default;
$chip-input-padding-y-sm: .125rem !default;
$chip-input-padding-x-sm: .5rem !default;
$chip-input-font-size-sm: $input-font-size-sm !default;
$chip-input-border-radius-sm: var(--cui-border-radius-sm) !default;
$chip-input-gap-sm: .125rem !default;
$chip-input-min-height-lg: $input-height-lg !default;
$chip-input-padding-y-lg: .375rem !default;
$chip-input-padding-x-lg: 1rem !default;
$chip-input-font-size-lg: $input-font-size-lg !default;
$chip-input-border-radius-lg: var(--cui-border-radius-lg) !default;
$chip-input-gap-lg: .5rem !default;API
Chip Input Module
import { NgModule } from '@angular/core';
import { ChipInputModule } from '@coreui/angular';
@NgModule({
imports: [ChipInputModule]
})
export class CustomAppModule {}Chip Input Standalone
import { Component, signal } from '@angular/core';
import { ChipInputComponent } from '@coreui/angular';
@Component({
template: `<c-chip-input aria-label="Tags" placeholder="Add a tag..." [(value)]="tags" />`,
imports: [ChipInputComponent]
})
export class CustomAppComponent {
readonly tags = signal(['Angular']);
}c-chip-input
component
import { ChipInputComponent } from '@coreui/angular'