cmdk-wc
v1.1.4
Published
Command menu web component built with Lit
Maintainers
Readme
cmdk-wc
A framework-agnostic command palette web component. Built as native Web Components with full TypeScript support, works in React, Vue, Svelte, Angular, and vanilla JavaScript. Inspired by cmdk, reimagined for any framework.
Demo
Table of Contents
- Demo
- Features
- About cmdk-wc
- Installation
- Quick Start
- Component API
- Data Structure
- Theming
- Keyboard Shortcuts
- TypeScript Support
- Browser Support
- Advanced Usage
- Framework Guides
- Building from Source
- Contributing
- License
- Support & Issues
Features
- 🎯 Native Web Components — Works across any framework or vanilla JS
- ⌨️ Keyboard Navigation — Full Cmd+K support, arrow keys, enter to select, escape to close with built-in visual keyboard shortcuts footer
- 🔄 Multi-Page Navigation — Drill-down into categories with breadcrumb support
- 🎨 Themeable — 14 CSS custom properties for complete visual customization, built-in dark mode
- 📦 Lightweight — ~15KB minified, zero external dependencies beyond Lit
- 🧩 Accessible — ARIA roles, screen reader support, semantic HTML, keyboard shortcuts footer with aria-labels
- ✨ TypeScript — Full type definitions included for all components and interfaces
- 🚀 Framework Agnostic — Drop into React, Vue, Svelte, Angular, or any framework
About cmdk-wc
cmdk-wc is a framework-agnostic command palette web component that works with any JavaScript framework or vanilla JavaScript.
- Published on NPM: npmjs.com/package/cmdk-wc
- License: MIT (Open Source Initiative)
- Repository: GitHub - AhmedAshraf-GPT/cmdk
- Built with: Lit 3.x - Web Components framework
- Type Support: Full TypeScript type definitions included
This component follows Web Components Standards (Custom Elements, Shadow DOM, Slot API) and is compatible with all modern browsers.
Installation
npm install cmdk-wcOr with your favorite package manager:
pnpm add cmdk-wc
yarn add cmdk-wc
bun add cmdk-wcQuick Start
Vanilla JavaScript
<!-- 1. Import the component -->
<script type="module">
import 'cmdk-wc';
</script>
<!-- 2. Use it in HTML -->
<cmdk-palette id="palette"></cmdk-palette>
<!-- 3. Provide data via JavaScript -->
<script>
const palette = document.getElementById('palette');
palette.pages = [
{
id: 'root',
lists: [
{
id: 'actions',
heading: 'Actions',
items: [
{ id: 'new', label: 'New File', icon: '✨' },
{ id: 'settings', label: 'Settings', icon: '⚙️' },
],
},
],
},
];
palette.addEventListener('cmdk-select', (event) => {
console.log('Selected:', event.detail);
});
// Open palette
palette.open = true;
</script>React
import { useRef, useEffect, useState } from 'react';
import 'cmdk-wc';
export default function App() {
const paletteRef = useRef<HTMLElement>(null);
const [open, setOpen] = useState(false);
useEffect(() => {
if (!paletteRef.current) return;
paletteRef.current.pages = [
{
id: 'root',
lists: [
{
id: 'actions',
heading: 'Actions',
items: [
{ id: 'new', label: 'New File', icon: '✨' },
{ id: 'settings', label: 'Settings', icon: '⚙️' },
],
},
],
},
];
}, []);
useEffect(() => {
if (paletteRef.current) {
paletteRef.current.open = open;
}
}, [open]);
useEffect(() => {
const handleSelect = (event: Event) => {
const customEvent = event as CustomEvent;
console.log('Selected:', customEvent.detail);
};
paletteRef.current?.addEventListener('cmdk-select', handleSelect);
return () =>
paletteRef.current?.removeEventListener('cmdk-select', handleSelect);
}, []);
return (
<div>
<button onClick={() => setOpen(true)}>Open Palette</button>
<cmdk-palette ref={paletteRef}></cmdk-palette>
</div>
);
}Note: See React Integration Guide for TypeScript setup and detailed patterns.
Vue 3
<template>
<div>
<button
@click="palette?.open ? (palette.open = false) : (palette.open = true)"
>
Open Palette
</button>
<cmdk-palette ref="paletteRef"></cmdk-palette>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import 'cmdk-wc';
const paletteRef = ref<HTMLElement>();
onMounted(() => {
if (!paletteRef.value) return;
paletteRef.value.pages = [
{
id: 'root',
lists: [
{
id: 'actions',
heading: 'Actions',
items: [
{ id: 'new', label: 'New File', icon: '✨' },
{ id: 'settings', label: 'Settings', icon: '⚙️' },
],
},
],
},
];
paletteRef.value.addEventListener('cmdk-select', (event: Event) => {
const customEvent = event as CustomEvent;
console.log('Selected:', customEvent.detail);
});
});
</script>Svelte
<script lang="ts">
import 'cmdk-wc';
import { onMount } from 'svelte';
let paletteRef: HTMLElement;
let open = false;
onMount(() => {
if (!paletteRef) return;
paletteRef.pages = [
{
id: 'root',
lists: [
{
id: 'actions',
heading: 'Actions',
items: [
{ id: 'new', label: 'New File', icon: '✨' },
{ id: 'settings', label: 'Settings', icon: '⚙️' },
]
}
]
}
];
paletteRef.addEventListener('cmdk-select', (event) => {
console.log('Selected:', event.detail);
});
});
$: {
if (paletteRef) {
paletteRef.open = open;
}
}
</script>
<button on:click={() => (open = !open)}>
Open Palette
</button>
<cmdk-palette bind:this={paletteRef}></cmdk-palette>Angular
// app.component.ts
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import 'cmdk-wc';
@Component({
selector: 'app-root',
template: `
<button (click)="togglePalette()">Open Palette</button>
<cmdk-palette #palette></cmdk-palette>
`,
})
export class AppComponent implements AfterViewInit {
@ViewChild('palette') paletteRef!: ElementRef<HTMLElement>;
ngAfterViewInit() {
const palette = this.paletteRef.nativeElement;
palette.pages = [
{
id: 'root',
lists: [
{
id: 'actions',
heading: 'Actions',
items: [
{ id: 'new', label: 'New File', icon: '✨' },
{ id: 'settings', label: 'Settings', icon: '⚙️' },
],
},
],
},
];
palette.addEventListener('cmdk-select', (event: Event) => {
const customEvent = event as CustomEvent;
console.log('Selected:', customEvent.detail);
});
}
togglePalette() {
this.paletteRef.nativeElement.open = !this.paletteRef.nativeElement.open;
}
}Component API
<cmdk-palette>
The root component that orchestrates state, keyboard navigation, and data filtering.
Properties
| Property | Type | Default | Description |
| -------- | ---------------- | -------- | ------------------------------------------------- |
| pages | CmdkPageData[] | [] | Array of page objects containing groups and items |
| open | boolean | false | Whether the palette is visible |
| page | string | 'root' | Current active page ID for drill-down navigation |
Methods
| Method | Signature | Description |
| -------------- | ------------ | --------------------------------------- |
| focusInput() | () => void | Programmatically focus the search input |
Events
| Event | Detail | Fired When |
| --------------- | ---------------------------------------------- | ----------------------------------------- |
| cmdk-select | { id: string; label: string; href?: string } | User selects a leaf item (non-navigation) |
| cmdk-navigate | { pageId: string } | User navigates to a new page |
| cmdk-open | {} | Palette opens |
| cmdk-close | {} | Palette closes |
Example
const palette = document.querySelector('cmdk-palette');
// Set data
palette.pages = [{ id: 'root', lists: [...] }];
// Open/close
palette.open = true;
// Listen for events
palette.addEventListener('cmdk-select', (event) => {
console.log('Item selected:', event.detail.label);
});<cmdk-input>
The search input field. Automatically rendered inside <cmdk-palette>. Handles text input and displays breadcrumb navigation on multi-page palettes.
Properties
| Property | Type | Default | Description |
| ------------- | -------- | ---------------------- | ---------------------- |
| placeholder | string | 'Search commands...' | Input placeholder text |
Methods
| Method | Signature | Description |
| --------- | ------------ | ----------------------- |
| focus() | () => void | Focus the input element |
| blur() | () => void | Blur the input element |
Events
| Event | Detail | Fired When |
| ------------ | ------------------- | ------------------------------ |
| cmdk-input | { value: string } | User types in the search field |
<cmdk-list>
Container for groups and free-search actions. Automatically rendered inside <cmdk-palette>. Handles scrolling and ARIA roles for accessibility.
Properties
| Property | Type | Default | Description | | -------- | ---- | ------- | -------------------- | | (none) | - | - | No public properties |
<cmdk-group>
Groups related items under a heading label. Automatically rendered for each list in the active page.
Properties
| Property | Type | Default | Description |
| -------- | -------- | ------- | ------------------ |
| label | string | '' | Group heading text |
Slots
| Slot | Content |
| --------- | ---------------------- |
| (default) | <cmdk-item> children |
<cmdk-item>
A single command or action row. Displays an icon (optional), label, and type hint (Link/Action/navigation).
Properties
| Property | Type | Default | Description |
| ---------- | -------------- | ------- | ----------------------------------------------------------------- |
| itemData | CmdkItemData | {} | The full item object (id, label, icon, href, page, onClick, etc.) |
| selected | boolean | false | Whether this item is currently highlighted |
Events
| Event | Detail | Fired When |
| ----------------- | -------------- | ---------------------------------------- |
| cmdk-item-click | CmdkItemData | User clicks or presses Enter on the item |
Example
interface CmdkItemData {
id: string;
label: string;
icon?: string; // Icon name or inline SVG
href?: string; // If set, type hint shows "Link"
page?: string; // If set, drill down to this page
closeOnSelect?: boolean; // Default: true
onClick?: () => void; // Optional side-effect
}<cmdk-free-search-action>
Fallback action shown when search yields no results. Allows users to perform a custom action with their search query.
Properties
| Property | Type | Default | Description |
| -------- | -------- | ------- | ----------------------------- |
| query | string | '' | The current search query text |
Example
<cmdk-palette id="palette"></cmdk-palette>
<script>
const palette = document.getElementById('palette');
palette.pages = [
{
id: 'root',
lists: [
{
id: 'actions',
heading: 'Actions',
items: [{ id: 'new', label: 'New', icon: '✨' }],
},
],
},
];
palette.addEventListener('cmdk-select', (event) => {
if (event.detail.id === 'free-search') {
console.log('User searched for:', event.detail.query);
}
});
</script>Data Structure
All data is provided as a nested structure of pages, groups, and items:
interface CmdkPageData {
id: string; // Unique page identifier
lists: CmdkListData[]; // Array of groups to display
}
interface CmdkListData {
id: string; // Unique group identifier
heading: string; // Label shown above items
items: CmdkItemData[]; // Array of commands
}
interface CmdkItemData {
id: string; // Unique item identifier
label: string; // Display text
icon?: string; // Icon name or inline SVG
href?: string; // Optional: if set, item is a link
page?: string; // Optional: navigate to page with this id
closeOnSelect?: boolean; // Default: true; set false for navigation items
onClick?: () => void; // Optional: side-effect on click
}Example:
const data = [
{
id: 'root',
lists: [
{
id: 'navigation',
heading: 'Navigation',
items: [
{ id: 'home', label: 'Home', icon: '🏠', href: '/' },
{ id: 'projects', label: 'Projects', icon: '📁', page: 'projects' },
],
},
{
id: 'actions',
heading: 'Actions',
items: [
{
id: 'new',
label: 'New File',
icon: '✨',
onClick: () => createFile(),
},
],
},
],
},
{
id: 'projects',
lists: [
{
id: 'recent',
heading: 'Recent Projects',
items: [
{ id: 'proj-1', label: 'My App', icon: '💻' },
{ id: 'proj-2', label: 'Website', icon: '🌐' },
],
},
],
},
];Theming
All visual styling is driven by CSS custom properties. Customize the palette's appearance by setting properties on the cmdk-palette element or :root.
CSS Custom Properties
| Property | Light Default | Dark Default | Description |
| ------------------------ | ----------------- | ----------------- | ----------------------------- |
| --cmdk-accent | #3b82f6 | #60a5fa | Primary accent color |
| --cmdk-accent-hover | #2563eb | #3b82f6 | Hover state for accent |
| --cmdk-text | #0f172a | #f1f5f9 | Primary text color |
| --cmdk-muted | #64748b | #94a3b8 | Secondary/muted text |
| --cmdk-border | #e2e8f0 | #334155 | Border color |
| --cmdk-surface | #f8fafc | #1e293b | Item hover background |
| --cmdk-bg | #ffffff | #0f172a | Primary background |
| --cmdk-overlay | rgba(0,0,0,0.4) | rgba(0,0,0,0.6) | Backdrop overlay |
| --cmdk-radius | 8px | 8px | Border radius |
| --cmdk-list-max-height | 400px | 400px | Max height of scrollable list |
Light Theme Example
cmdk-palette {
--cmdk-accent: #3b82f6;
--cmdk-accent-hover: #2563eb;
--cmdk-text: #0f172a;
--cmdk-muted: #64748b;
--cmdk-border: #e2e8f0;
--cmdk-surface: #f8fafc;
--cmdk-bg: #ffffff;
--cmdk-overlay: rgba(0, 0, 0, 0.4);
--cmdk-radius: 8px;
--cmdk-list-max-height: 400px;
}Dark Theme Example
@media (prefers-color-scheme: dark) {
cmdk-palette {
--cmdk-accent: #60a5fa;
--cmdk-accent-hover: #3b82f6;
--cmdk-text: #f1f5f9;
--cmdk-muted: #94a3b8;
--cmdk-border: #334155;
--cmdk-surface: #1e293b;
--cmdk-bg: #0f172a;
--cmdk-overlay: rgba(0, 0, 0, 0.6);
--cmdk-radius: 8px;
--cmdk-list-max-height: 400px;
}
}Custom Theme (Purple)
cmdk-palette {
--cmdk-accent: #8b5cf6;
--cmdk-accent-hover: #7c3aed;
--cmdk-bg: #fef5ff;
--cmdk-text: #3f0f5c;
--cmdk-surface: #f3e8ff;
--cmdk-border: #e9d5ff;
}
@media (prefers-color-scheme: dark) {
cmdk-palette {
--cmdk-accent: #d8b4fe;
--cmdk-accent-hover: #f0abfc;
--cmdk-bg: #2d0a4e;
--cmdk-text: #f3e8ff;
--cmdk-surface: #4c1d95;
--cmdk-border: #6b21a8;
}
}For complete theming guidance, see the Theming Guide.
Keyboard Shortcuts
| Key | Behavior | | ---------------------------------- | ------------------------------------ | | Cmd/Ctrl + K | Toggle palette open/closed (global) | | ↑ / ↓ | Navigate items up/down | | Enter | Select highlighted item | | Esc | Go back a page or close palette | | Backspace | Clear search (when input is focused) |
TypeScript Support
Full TypeScript definitions are included. Import types from the package to ensure type safety:
import type { CmdkPageData, CmdkListData, CmdkItemData } from 'cmdk-wc';
const pages: CmdkPageData[] = [
{
id: 'root',
lists: [
{
id: 'actions',
heading: 'Actions',
items: [
{
id: 'new',
label: 'New File',
icon: '✨',
onClick: () => console.log('Creating new file'),
},
],
},
],
},
];Framework-Specific Setup
- React: See React Integration for type declaration setup
- Vue: Native support, full IntelliSense in templates
- Svelte: Native support, full IntelliSense in templates
- Angular: Use
CUSTOM_ELEMENTS_SCHEMAto avoid template errors
Browser Support
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- All modern mobile browsers
Web Components are widely supported. Check caniuse.com for detailed compatibility.
Advanced Usage
Multi-Page Navigation
Create drill-down experiences by providing multiple pages and using the page property on items:
const palette = document.querySelector('cmdk-palette');
palette.pages = [
{
id: 'root',
lists: [
{
heading: 'Pages',
items: [{ id: 'go-projects', label: 'Projects', page: 'projects' }],
},
],
},
{
id: 'projects',
lists: [
{ heading: 'Your Projects', items: [{ id: 'p1', label: 'My App' }] },
],
},
];
palette.open = true;Custom Actions with onClick
Execute custom side-effects when items are selected:
palette.pages = [
{
id: 'root',
lists: [
{
heading: 'Actions',
items: [
{
id: 'create',
label: 'Create New File',
onClick: () => {
fs.writeFile('newfile.txt', '', () => {
console.log('File created!');
});
},
},
],
},
],
},
];Listening to Events
palette.addEventListener('cmdk-select', (event) => {
console.log('Selected:', event.detail);
// { id: 'new', label: 'New File', href?: '...' }
});
palette.addEventListener('cmdk-navigate', (event) => {
console.log('Navigated to page:', event.detail.pageId);
});
palette.addEventListener('cmdk-open', () => console.log('Opened'));
palette.addEventListener('cmdk-close', () => console.log('Closed'));Framework Guides
Detailed integration guides for each framework:
Contributing
This repository is currently not open for contributions.
Please open an issue to discuss before submitting any pull requests.
License
MIT License (Open Source Initiative Approved)
This software is released under the MIT License, which permits commercial use, modification, and distribution.
You are free to:
- ✅ Use this software for any purpose (personal, educational, commercial)
- ✅ Modify and distribute modified versions
- ✅ Include this software in proprietary applications
Conditions:
- Include a copy of the License and Copyright notice with distributions
See the full LICENSE file for details.
Support & Issues
- Bug Reports: GitHub Issues
- Feature Requests: GitHub Discussions
- Documentation: See docs/ for framework-specific guides
Building from Source
Prerequisites
- Node.js 18+
- pnpm (or npm/yarn/bun)
Setup
git clone https://github.com/AhmedAshraf-GPT/cmdk.git
cd cmdk
pnpm installDevelopment
# Start Vite dev server with hot reload
pnpm dev
# Build library
pnpm build
# Run tests (if applicable)
pnpm test
# Lint code
pnpm lintProject Structure
src/
├── components/ # Web component definitions
│ ├── cmdkpalette.ts # Main palette orchestrator
│ ├── cmdkinput.ts # Search input
│ ├── cmdklist.ts # Item list container
│ ├── cmdkgroup.ts # Item group
│ ├── cmdkitem.ts # Individual item
│ └── ...
├── index.ts # Entry point
└── types.ts # TypeScript definitions
types/
├── index.d.ts # Main type definitions
├── react.d.ts # React integration types
├── vue.d.ts # Vue integration types
└── ... # Other framework types
docs/
├── react.md # React integration guide
├── vue.md # Vue 3 integration guide
├── svelte.md # Svelte integration guide
├── angular.md # Angular integration guide
├── vanilla.md # Vanilla JS guide
└── theming.md # CSS customization guideMaintenance & Updates
This project is actively maintained. For version history and changelog, see GitHub Releases.
Current Version
v1.1.0 - Latest release with full keyboard navigation, accessibility features, and framework integrations.
Inspiration
Inspired by cmdk by Paco Coursey, reimagined as a framework-agnostic Web Component.
