<p align="center"> <img width="20%" height="20%" src="./src/assets/ngneat%20cdk.svg"> </p>
npm install @ngneat/cmdk






![spectator]()


> Fast, composable, unstyled command menu for Angular. Directly inspired from pacocoursey/cmdk
@ngneat/cmdk is a command menu Angular component that can also be used as an accessible combobox. You render items, it filters and sorts them automatically. @ngneat/cmdk supports a fully composable API, so you can wrap items in other components or even as static HTML.
Demo and examples: ngneat.github.io/cmdk
- π¨ Un-styled, so that you can provide your own styles easily
- π₯ Provides wrapper, so that you can pass your own template, component or static HTML
- π Default filtering present
- πΌοΈ Drop in stylesheet themes provided
- βΏ Accessible
| @ngneat/cmdk | Angular |
| -- | -- |
| 1.x | <16 |
| 2.x | >=16 |
``bash`
ng add @ngneat/cmdk
`bashFor Angular version < 16
$3
Same as
npm, just instead of npm install, write yarn add.Usage
$3
#### 1.1 Module Setup
> This is taken care with ng add @ngneat/cmdk
`ts
import { CmdkModule } from '@ngneat/cmdk';@NgModule({
imports: [
CmdkModule,
],
})
export class AppModule {}
`#### 1.2 Standalone Setup
`typescript
import { AppComponent } from './src/app.component';import {
CommandComponent,
GroupComponent,
InputDirective,
ItemDirective,
ListComponent,
EmptyDirective
} from '@ngneat/cmdk';
@Component({
selector: 'app-root',
standalone: true,
imports: [
CommandComponent,
InputDirective,
ListComponent,
GroupComponent,
ItemDirective,
EmptyDirective
],
})
`$3
`html
No results found.
`Components
Each component has a specific class (starting with
cmdk-) that can be used for styling.$3
Render this to show the command menu.
| Selector | Class |
| -------------- | --------------- |
|
cmdk-command | .cmdk-command |#### Properties
| Name | Description |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
@Input() ariaLabel: string | Accessible Label for this command menu. Not shown visibly. |
| @Input() filter?: ((value: string, search: string) => boolean) | Custom filter function for whether each command menu item should matches the given search query. It should return a boolean, false being hidden entirely. You can pass null to disable default filtering.
Default: (value, search) => value.toLowerCase().includes(search.toLowerCase()) |
| @Input() value?: string | Optional controlled state of the selected command menu item. |
| @Input() loading?: boolean | Optional indicator to show loader |
| @Input() loop?: boolean | Optionally set to true to turn on looping around when using the arrow keys. |
| @Output() valueChanged: EventEmitter | Event handler called when the selected item of the menu changes. |$3
Render this to show the command input.
| Selector | Class |
| ------------------ | ------------- |
|
input[cmdkinput] | .cmdk-input |#### Properties
| Name | Description |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| @Input() updateOn: 'blur' | 'change' | 'input' | Optional indicator to provide event listener when filtering should happen.
Default:
input |$3
Contains items and groups.
| Selector | Class |
| ----------- | ------------ |
|
cmdk-list | .cmdk-list |Animate height using the
--cmdk-list-height CSS variable.`css
.cmdk-list {
min-height: 300px;
height: var(--cmdk-list-height);
max-height: 500px;
transition: height 100ms ease;
}
`To scroll item into view earlier near the edges of the viewport, use scroll-padding:
`css
.cmdk-list {
scroll-padding-block-start: 8px;
scroll-padding-block-end: 8px;
}
`#### Properties
| Name | Description |
| ----------------------------- | ---------------------------------------------------------- |
|
@Input() ariaLabel?: string | Accessible Label for this command menu. Not shown visibly. |$3
Item that becomes active on pointer enter. You should provide a unique
value for each item, but it will be automatically inferred from the .textContent.Items will not unmount from the DOM, rather the
cmdk-hidden attribute is applied to hide it from view. This may be relevant in your styling.| State | Selector | Class |
| --------------------- | --------------------------- | --------------------- |
| Default |
[cmdkItem] | .cmdk-item |
| Active | [cmdkItem][aria-selected] | .cmdk-item-active |
| Filtered | [cmdkItem] | .cmdk-item-filtered |
| Disabled | [cmdkItem] | .cmdk-item-disabled |
| Hidden (not-filtered) | [cmdkItem][cmdk-hidden] | |#### Properties
| Name | Description |
| -------------------------------------------- | -------------------------------------------------------------------------------- |
| value: string | undefined; | Contextual Value of the list-item |
|
@Input() disabled: boolean | Contextually mark the item as disabled. Keyboard navigation will skip this item. |
| @Input() filtered: boolean | Contextually mark the item as filtered. |
| @Output() selected: EventEmitter | Event handler called when the item is selected |$3
Groups items together with the given label (
.cmdk-group-label).| Selector | Class |
| ------------ | ------------- |
|
cmdk-group | .cmdk-group |Groups will not unmount from the DOM, rather the
cmdk-hidden attribute is applied to hide it from view. This may be relevant in your styling.#### Properties
| Name | Description |
| ----------------------------- | ---------------------------------------------------------- |
|
@Input() label: Content | Label for this command group. Can be HTML string |
| @Input() ariaLabel?: string | Accessible Label for this command menu. Not shown visibly. |$3
Automatically renders when there are no results for the search query.
| Selector | Class |
| ------------ | ------------- |
|
*cmdkEmpty | .cmdk-empty |$3
This will be conditionally renderer when you pass
loading=true with cmdk-command| Selector | Class |
| ------------- | -------------- |
|
*cmdkLoader | .cmdk-loader |Examples
Code snippets for common use cases.
$3
Often selecting one item should navigate deeper, with a more refined set of items. For example selecting "Change themeβ¦" should show new items "Dark theme" and "Light theme". We call these sets of items "pages", and they can be implemented with simple state:
`html
``ts
pages: Array = [];
search = '';get page() {
return this.pages[this.pages.length - 1];
}
onKeyDown(e: KeyboardEvent) {
// Escape goes to previous page
// Backspace goes to previous page when search is empty
if (e.key === 'Escape' || (e.key === 'Backspace' && !this.search)) {
e.preventDefault();
this.pages = this.pages.slice(0, -1);
}
}
setSearch(ev: Event) {
this.search = (ev.target as HTMLInputElement)?.value;
}
setPages(page: string) {
this.pages.push(page);
}
`$3
Render the items as they become available. Filtering and sorting will happen automatically.
`html
Fetching words...
``ts
loading = false;getItems() {
this.loading = true;
setTimeout(() => {
this.items = ['A', 'B', 'C', 'D'];
this.loading = false;
}, 3000);
}
`$3
We recommend using the Angular CDK Overlay. @ngneat/cdk relies on the Angular CDK, so this will reduce your bundle size a bit due to shared dependencies.
First, configure the trigger component:
`html
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="isDialogOpen"
>
``ts
isDialogOpen = false;listener(e: KeyboardEvent) {
if (e.key === 'k' && (e.metaKey || e.altKey)) {
e.preventDefault();
if (this.isDialogOpen) {
this.isDialogOpen = false;
} else {
this.isDialogOpen = true;
}
}
}
ngOnInit() {
document.addEventListener('keydown', (ev) => this.listener(ev));
}
ngOnDestroy() {
document.removeEventListener('keydown', (ev) => this.listener(ev));
}
`Then, render the
cmdk-command inside CDK Overlay content:`html
``ts
readonly items: Array<{ label: string }> = [
{
label: 'Open Application',
},
{
label: 'Show in Finder',
},
{
label: 'Show Info in Finder',
},
{
label: 'Add to Favorites',
},
];ngAfterViewInit() {
this.input.nativeElement.focus();
}
`$3
You can find global stylesheets to drop in as a starting point for styling. See ngneat/cmdk/styles for examples.
You can include the
SCSS stylesheet in your application's style file:`scss
// Global is needed for any theme
@use "~@ngneat/cmdk/styles/scss/globals";// Then add theme
@use "~@ngneat/cmdk/styles/scss/framer";
// @use "~@ngneat/cmdk/styles/scss/vercel";
// @use "~@ngneat/cmdk/styles/scss/linear";
// @use "~@ngneat/cmdk/styles/scss/raycast";
`or, use pre-built
CSS file in angular.json`json
// ...
"styles": [
"...",
"node_modules/@ngneat/cmdk/styles/globals.css"
"node_modules/@ngneat/cmdk/styles/framer.css"
],
// ...
`FAQ
Accessible? Yes. Labeling, aria attributes, and DOM ordering tested with Voice Over and Chrome DevTools.
Virtualization? No. Good performance up to 2,000-3,000 items, though. Read below to bring your own.
Filter/sort items manually? Yes. Pass
filter={yourFilter} to Command. Better memory usage and performance. Bring your own virtualization this way.Unstyled? Yes, use the listed CSS selectors.
Weird/wrong behavior? Make sure your
[cdkItem] has a unique value`.Listen for βK automatically? No, do it yourself to have full control over keybind context.
Thanks goes to these wonderful people (emoji key):
Dharmen Shah οΈοΈοΈοΈβΏοΈ π» π π¨ π π‘ π€ π§ π¦ π π¬ | Netanel Basal π¬ πΌ π π€ π§ π§βπ« π π¬ π | Paco π¨ π π€ π¬ |
This project follows the all-contributors specification. Contributions of any kind welcome!