A production-ready Angular multiselect dropdown component with search, virtual scroll, keyboard navigation, group support, and full theming.
- Multi-select and single-select modes
- Search/filter with configurable placeholder
- Virtual scrolling (CDK) auto-activated for lists > 100 items
- Full keyboard navigation — ArrowUp/Down, Enter, Escape, Tab
- ControlValueAccessor — works with
ngModelandReactiveFormsModule - Select all / Deselect all
- Badge display with overflow indicator (+N)
- Group headers from
option.group - Disabled options and disabled component state
- Tooltip support via
option.tooltip - Loading spinner state
- Dropdown position — auto (smart), top, or bottom
- Angular animations for smooth open/close
- ARIA roles and attributes for accessibility
- CSS custom properties for complete theming
- Tailwind CSS integration support
- Angular Material theme built-in
- Angular 8 – 20+ compatible (partial-Ivy compilation)
npm install @apps24/ng-select-pro
⚠️ @angular/cdkrequired peer dependencyThis library uses
@angular/cdkfor virtual scrolling. If your project does not already have@angular/cdkinstalled, install it first at the same major version as your Angular to avoid peer dependency conflicts:npm install @angular/cdk@16 # for Angular 16 npm install @angular/cdk@17 # for Angular 17 npm install @angular/cdk@18 # for Angular 18 npm install @angular/cdk@19 # for Angular 19 npm install @angular/cdk@20 # for Angular 20Not sure which version you have? Run
ng versionin your project directory.
Module-based app (Angular 8–16):
// app.module.ts
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MultiSelectModule } from '@apps24/ng-select-pro';
@NgModule({
imports: [
BrowserAnimationsModule, // required for dropdown animation
FormsModule,
MultiSelectModule
]
})
export class AppModule {}Standalone app (Angular 14+):
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { importProvidersFrom } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MultiSelectModule } from '@apps24/ng-select-pro';
bootstrapApplication(AppComponent, {
providers: [
provideAnimations(),
importProvidersFrom(FormsModule, MultiSelectModule)
]
});// app.component.ts
import { Component } from '@angular/core';
import { IMultiSelectOption } from '@apps24/ng-select-pro';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
selected: IMultiSelectOption[] = [];
options: IMultiSelectOption[] = [
{ id: 1, label: 'Angular' },
{ id: 2, label: 'React' },
{ id: 3, label: 'Vue' }
];
}<!-- app.component.html -->
<ng-multiselect
[(ngModel)]="selected"
[options]="options"
></ng-multiselect>
<p>Selected: {{ selected | json }}</p>A complete example showing multi-select, single-select, grouped options, reactive forms, max selection, disabled items, and event handling — all in one component.
// user-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { IMultiSelectOption, IMultiSelectConfig } from '@apps24/ng-select-pro';
@Component({
selector: 'app-user-form',
templateUrl: './user-form.component.html',
styleUrls: ['./user-form.component.scss']
})
export class UserFormComponent implements OnInit {
form!: FormGroup;
// ── Skills multi-select ──────────────────────────────────────────────────
skillOptions: IMultiSelectOption[] = [
{ id: 'angular', label: 'Angular', group: 'Frontend' },
{ id: 'react', label: 'React', group: 'Frontend' },
{ id: 'vue', label: 'Vue.js', group: 'Frontend' },
{ id: 'nodejs', label: 'Node.js', group: 'Backend' },
{ id: 'python', label: 'Python', group: 'Backend' },
{ id: 'go', label: 'Go', group: 'Backend' },
{ id: 'postgres', label: 'PostgreSQL', group: 'Database' },
{ id: 'mongo', label: 'MongoDB', group: 'Database' },
{ id: 'redis', label: 'Redis', group: 'Database' },
{ id: 'docker', label: 'Docker', group: 'DevOps' },
{ id: 'k8s', label: 'Kubernetes', group: 'DevOps' },
];
skillConfig: Partial<IMultiSelectConfig> = {
placeholder: 'Select your skills',
searchPlaceholder: 'Search skills...',
maxSelect: 5,
maxDisplayed: 3,
showSelectAll: false,
showClearAll: true
};
// ── Country single-select ────────────────────────────────────────────────
countryOptions: IMultiSelectOption[] = [
{ id: 'us', label: 'United States', group: 'Americas' },
{ id: 'ca', label: 'Canada', group: 'Americas' },
{ id: 'gb', label: 'United Kingdom',group: 'Europe' },
{ id: 'de', label: 'Germany', group: 'Europe' },
{ id: 'fr', label: 'France', group: 'Europe' },
{ id: 'jp', label: 'Japan', group: 'Asia' },
{ id: 'in', label: 'India', group: 'Asia' },
];
countryConfig: Partial<IMultiSelectConfig> = {
mode: 'single',
placeholder: 'Select country',
closeOnSelect: true,
showSelectAll: false,
showClearAll: true
};
// ── Role select (disabled items) ─────────────────────────────────────────
roleOptions: IMultiSelectOption[] = [
{ id: 'dev', label: 'Developer' },
{ id: 'lead', label: 'Tech Lead' },
{ id: 'mgr', label: 'Engineering Manager' },
{ id: 'arch', label: 'Architect' },
{ id: 'cto', label: 'CTO', disabled: true, tooltip: 'Contact sales' },
{ id: 'vp', label: 'VP Eng', disabled: true, tooltip: 'Contact sales' },
];
roleConfig: Partial<IMultiSelectConfig> = {
mode: 'single',
placeholder: 'Select your role',
closeOnSelect: true,
showSelectAll: false
};
// ── Event log ─────────────────────────────────────────────────────────────
events: string[] = [];
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.form = this.fb.group({
name: ['', Validators.required],
skills: [[], Validators.required],
country: [null, Validators.required],
role: [null, Validators.required]
});
}
onSkillsChange(selected: IMultiSelectOption[]): void {
this.events.unshift(`Skills changed → ${selected.map(s => s.label).join(', ') || 'none'}`);
}
onCountryChange(selected: IMultiSelectOption[]): void {
this.events.unshift(`Country changed → ${selected[0]?.label || 'none'}`);
}
onSubmit(): void {
if (this.form.valid) {
console.log('Form value:', this.form.value);
this.events.unshift('Form submitted ✓');
}
}
}<!-- user-form.component.html -->
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="form">
<!-- Name field -->
<div class="field">
<label>Full Name</label>
<input formControlName="name" type="text" placeholder="John Doe" />
</div>
<!-- Skills — multi-select, max 5, grouped, with search -->
<div class="field">
<label>Skills <small>(max 5)</small></label>
<ng-multiselect
formControlName="skills"
[options]="skillOptions"
[config]="skillConfig"
(selectionChange)="onSkillsChange($event)"
></ng-multiselect>
<small *ngIf="form.get('skills')?.invalid && form.get('skills')?.touched">
Please select at least one skill.
</small>
</div>
<!-- Country — single-select with groups -->
<div class="field">
<label>Country</label>
<ng-multiselect
formControlName="country"
[options]="countryOptions"
[config]="countryConfig"
(selectionChange)="onCountryChange($event)"
></ng-multiselect>
</div>
<!-- Role — single-select with some disabled options -->
<div class="field">
<label>Role</label>
<ng-multiselect
formControlName="role"
[options]="roleOptions"
[config]="roleConfig"
></ng-multiselect>
</div>
<button type="submit" [disabled]="form.invalid">Submit</button>
</form>
<!-- Event log -->
<div class="event-log" *ngIf="events.length">
<h4>Events</h4>
<p *ngFor="let e of events">{{ e }}</p>
</div>/* user-form.component.scss */
.form {
max-width: 480px;
display: flex;
flex-direction: column;
gap: 20px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
label {
font-weight: 600;
font-size: 14px;
color: #374151;
}
small {
color: #ef4444;
font-size: 12px;
}
input {
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
outline: none;
&:focus { border-color: #3b82f6; }
}
}
button[type="submit"] {
padding: 10px 24px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
&:disabled { opacity: 0.5; cursor: not-allowed; }
&:hover:not(:disabled) { background: #2563eb; }
}
.event-log {
margin-top: 24px;
padding: 12px;
background: #f8fafc;
border-radius: 8px;
font-size: 13px;
color: #475569;
}// user-form.module.ts (or add to AppModule)
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MultiSelectModule } from '@apps24/ng-select-pro';
import { UserFormComponent } from './user-form.component';
@NgModule({
declarations: [UserFormComponent],
imports: [
CommonModule,
ReactiveFormsModule,
BrowserAnimationsModule,
MultiSelectModule
],
exports: [UserFormComponent]
})
export class UserFormModule {}selectedFruits: IMultiSelectOption[] = [];
fruits: IMultiSelectOption[] = [
{ id: 1, label: 'Apple' },
{ id: 2, label: 'Banana' },
{ id: 3, label: 'Cherry' }
];<ng-multiselect [(ngModel)]="selectedFruits" [options]="fruits"></ng-multiselect>
<p>You selected: {{ selectedFruits.map(o => o.label).join(', ') }}</p><ng-multiselect
[(ngModel)]="selectedCountry"
[options]="countryOptions"
[config]="{ mode: 'single', closeOnSelect: true, showSelectAll: false }"
></ng-multiselect><ng-multiselect
[(ngModel)]="selectedTags"
[options]="tagOptions"
[config]="{ maxSelect: 3, placeholder: 'Pick up to 3 tags' }"
></ng-multiselect>isLoading = true;
options: IMultiSelectOption[] = [];
ngOnInit() {
this.api.getOptions().subscribe(data => {
this.options = data;
this.isLoading = false;
});
}<ng-multiselect
[(ngModel)]="selected"
[options]="options"
[config]="{ loading: isLoading, loadingText: 'Fetching options...' }"
></ng-multiselect>config: Partial<IMultiSelectConfig> = {
itemRenderer: (opt) => `<strong>${opt.label}</strong> <small style="color:#9ca3af">#${opt.id}</small>`
};<ng-multiselect [(ngModel)]="selected" [options]="options" [config]="config"></ng-multiselect><ng-multiselect
[(ngModel)]="selected"
[options]="options"
(selectionChange)="onSelectionChange($event)"
(searchChange)="onSearch($event)"
(dropdownOpen)="onOpen()"
(dropdownClose)="onClose()"
></ng-multiselect>onSelectionChange(selected: IMultiSelectOption[]) {
console.log('Selected:', selected);
}
onSearch(query: string) {
// Can be used to trigger remote search
console.log('Search query:', query);
}interface IMultiSelectOption {
id: any; // Unique identifier
label: string; // Display text
disabled?: boolean;// Prevent selection (greyed out)
group?: string; // Group header label
image?: string; // Image URL shown in option row
tooltip?: string; // Native browser tooltip on hover
data?: any; // Store any extra data (not rendered)
}All options are passed via [config]="{ ... }":
| Option | Type | Default | Description |
|---|---|---|---|
mode |
'multi' | 'single' |
'multi' |
Selection mode |
searchEnabled |
boolean |
true |
Show search input |
searchPlaceholder |
string |
'Search...' |
Search input placeholder |
placeholder |
string |
'Select options' |
Placeholder when empty |
maxSelect |
number | null |
null |
Max selections (null = unlimited) |
maxDisplayed |
number |
3 |
Max badges shown before +N overflow |
disabled |
boolean |
false |
Disable entire component |
closeOnSelect |
boolean |
false |
Auto-close after each selection |
showSelectAll |
boolean |
true |
Show Select All link (multi only) |
showClearAll |
boolean |
true |
Show × clear button |
labelRenderer |
((opt) => string) | null |
null |
Custom badge label function |
itemRenderer |
((opt) => string) | null |
null |
Custom option HTML (supports HTML) |
noResultsText |
string |
'No results found' |
Empty search state text |
loadingText |
string |
'Loading...' |
Loading state text |
loading |
boolean |
false |
Show loading spinner |
autoClose |
boolean |
true |
Close on outside click |
dropdownPosition |
'auto' | 'top' | 'bottom' |
'auto' |
Dropdown direction |
theme |
IMultiSelectTheme |
— | CSS class overrides per slot |
| Key | Applied to |
|---|---|
containerClass |
Trigger container div |
dropdownClass |
Dropdown panel div |
searchClass |
Search <input> |
itemClass |
Each option row |
selectedItemClass |
Selected option rows |
disabledItemClass |
Disabled option rows |
badgeClass |
Each selected badge |
overflowBadgeClass |
The +N overflow badge |
clearBtnClass |
Clear all × button |
selectAllClass |
Select all row |
| Output | Type | When |
|---|---|---|
selectionChange |
EventEmitter<IMultiSelectOption[]> |
Selection changes |
searchChange |
EventEmitter<string> |
User types in search |
dropdownOpen |
EventEmitter<void> |
Dropdown opens |
dropdownClose |
EventEmitter<void> |
Dropdown closes |
Set defaults once for the entire app:
import { MULTI_SELECT_DEFAULT_CONFIG } from '@apps24/ng-select-pro';
// app.module.ts
providers: [
{
provide: MULTI_SELECT_DEFAULT_CONFIG,
useValue: {
placeholder: 'Choose...',
maxDisplayed: 2,
showSelectAll: false
}
}
]Or use forRoot():
MultiSelectModule.forRoot({
placeholder: 'Choose...',
maxDisplayed: 2,
showSelectAll: false
})Override any visual value on :root or a parent selector:
:root {
--ng-select-border: 1px solid #6366f1;
--ng-select-border-radius: 12px;
--ng-select-badge-bg: #eef2ff;
--ng-select-badge-text: #4f46e5;
--ng-select-item-hover-bg: #eef2ff;
--ng-select-item-selected-bg: #e0e7ff;
--ng-select-item-selected-text: #3730a3;
--ng-select-font-size: 13px;
--ng-select-max-height: 320px;
}| Variable | Default | Purpose |
|---|---|---|
--ng-select-border |
1px solid #d1d5db |
Container border |
--ng-select-border-radius |
8px |
Corner radius |
--ng-select-bg |
#ffffff |
Container background |
--ng-select-text |
#111827 |
Text color |
--ng-select-placeholder |
#9ca3af |
Placeholder color |
--ng-select-dropdown-bg |
#ffffff |
Dropdown background |
--ng-select-dropdown-shadow |
subtle shadow | Dropdown elevation |
--ng-select-item-hover-bg |
#f9fafb |
Hovered option bg |
--ng-select-item-selected-bg |
#eff6ff |
Selected option bg |
--ng-select-item-selected-text |
#1d4ed8 |
Selected option text |
--ng-select-badge-bg |
#dbeafe |
Badge background |
--ng-select-badge-text |
#1e40af |
Badge text |
--ng-select-badge-radius |
4px |
Badge corner radius |
--ng-select-search-border |
1px solid #e5e7eb |
Search input border |
--ng-select-disabled-opacity |
0.5 |
Disabled opacity |
--ng-select-font-size |
14px |
Base font size |
--ng-select-max-height |
280px |
Dropdown max height |
See Tailwind Integration Guide.
Add .ng-select-material class to the host:
<ng-multiselect class="ng-select-material" ...></ng-multiselect>See Angular Material Integration Guide.
| Key | Action |
|---|---|
↓ ArrowDown |
Open dropdown / move focus down |
↑ ArrowUp |
Move focus up |
Enter |
Open dropdown / select focused option |
Escape |
Close dropdown |
Tab |
Close dropdown and move focus |
- Fork the repo
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes with tests
- Run tests:
npm test - Run lint:
npm run lint - Submit a PR against
main
git clone https://github.com/apps24/ng-select-pro.git
cd ng-select-pro
npm install
npm run build:lib # Build the library
npm start # Start demo app at localhost:4200
npm test # Run library unit testsMIT © apps24