UI Components Medium
Command Palette (⌘K)
A keyboard-driven command palette modal triggered by ⌘K / Ctrl+K, with fuzzy search, keyboard navigation, and smooth open/close animation.
Open in Lab
MCP
vanilla-js css keyboard-events
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0f172a;
color: #f1f5f9;
min-height: 100vh;
display: grid;
place-items: center;
}
.page-hint {
color: #64748b;
font-size: 0.9rem;
}
kbd {
display: inline-block;
font-family: inherit;
font-size: 0.8em;
padding: 0.1em 0.45em;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 0.3em;
color: #94a3b8;
line-height: 1.5;
}
/* ── Overlay ── */
.cp-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
display: grid;
place-items: start center;
padding-top: 15vh;
z-index: 9999;
animation: cp-fade-in 0.15s ease;
}
.cp-overlay[hidden] {
display: none;
}
@keyframes cp-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* ── Modal ── */
.cp-modal {
width: min(560px, calc(100vw - 2rem));
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1rem;
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04);
overflow: hidden;
animation: cp-slide-in 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes cp-slide-in {
from {
opacity: 0;
transform: scale(0.96) translateY(-8px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* ── Search row ── */
.cp-search-wrap {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.875rem 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.cp-search-icon {
color: #64748b;
flex-shrink: 0;
}
.cp-input {
flex: 1;
background: transparent;
border: none;
outline: none;
font-size: 1rem;
color: #f1f5f9;
caret-color: #38bdf8;
}
.cp-input::placeholder {
color: #475569;
}
.cp-esc-hint {
font-size: 0.7rem;
color: #475569;
flex-shrink: 0;
}
/* ── Results list ── */
.cp-list {
list-style: none;
max-height: 320px;
overflow-y: auto;
padding: 0.5rem;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}
.cp-group-label {
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.08em;
color: #475569;
text-transform: uppercase;
padding: 0.5rem 0.625rem 0.25rem;
}
.cp-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 0.75rem;
border-radius: 0.5rem;
cursor: pointer;
color: #cbd5e1;
transition: background 0.1s ease, color 0.1s ease;
font-size: 0.9rem;
}
.cp-item:hover,
.cp-item.active {
background: rgba(56, 189, 248, 0.1);
color: #f1f5f9;
}
.cp-item.active {
outline: none;
}
.cp-item-icon {
font-size: 1rem;
width: 1.25rem;
text-align: center;
flex-shrink: 0;
}
.cp-item-text {
flex: 1;
}
.cp-item-name {
display: block;
}
.cp-item-meta {
display: block;
font-size: 0.75rem;
color: #475569;
}
.cp-item-kbd {
font-size: 0.7rem;
color: #475569;
flex-shrink: 0;
}
.cp-no-results {
text-align: center;
color: #475569;
padding: 2rem 1rem;
font-size: 0.9rem;
}
/* ── Footer ── */
.cp-footer {
display: flex;
gap: 1.25rem;
align-items: center;
padding: 0.625rem 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.07);
font-size: 0.75rem;
color: #475569;
}
.cp-footer kbd {
font-size: 0.7em;
padding: 0.1em 0.35em;
}(function () {
"use strict";
// ── Commands dataset ───────────────────────────────────────────
const COMMANDS = [
{ group: "Navigation", icon: "🏠", name: "Go to Home", meta: "", shortcut: "G H" },
{ group: "Navigation", icon: "📚", name: "Open Library", meta: "", shortcut: "G L" },
{ group: "Navigation", icon: "⚙️", name: "Settings", meta: "", shortcut: "G S" },
{ group: "Navigation", icon: "📖", name: "Documentation", meta: "", shortcut: "" },
{ group: "Actions", icon: "✨", name: "New Resource", meta: "Create", shortcut: "⌘N" },
{ group: "Actions", icon: "🔍", name: "Search Everything", meta: "Find", shortcut: "⌘F" },
{ group: "Actions", icon: "📋", name: "Copy Page URL", meta: "Share", shortcut: "⌘U" },
{ group: "Actions", icon: "🌙", name: "Toggle Dark Mode", meta: "Theme", shortcut: "⌘D" },
{ group: "Help", icon: "❓", name: "Keyboard Shortcuts", meta: "", shortcut: "?" },
{ group: "Help", icon: "💬", name: "Open Feedback", meta: "", shortcut: "" },
];
// ── Elements ───────────────────────────────────────────────────
const overlay = document.getElementById("cp-overlay");
const input = document.getElementById("cp-input");
const list = document.getElementById("cp-list");
let activeIndex = -1;
// ── Open / close ───────────────────────────────────────────────
function open() {
overlay.hidden = false;
input.value = "";
render(COMMANDS);
activeIndex = -1;
requestAnimationFrame(() => input.focus());
}
function close() {
overlay.hidden = true;
input.blur();
}
// ── Render results ─────────────────────────────────────────────
function render(commands) {
list.innerHTML = "";
if (commands.length === 0) {
list.innerHTML = '<li class="cp-no-results">No commands found</li>';
return;
}
let currentGroup = null;
commands.forEach((cmd, i) => {
if (cmd.group !== currentGroup) {
currentGroup = cmd.group;
const label = document.createElement("li");
label.className = "cp-group-label";
label.textContent = cmd.group;
list.appendChild(label);
}
const li = document.createElement("li");
li.className = "cp-item";
li.setAttribute("role", "option");
li.dataset.index = i;
li.innerHTML = `
<span class="cp-item-icon">${cmd.icon}</span>
<span class="cp-item-text">
<span class="cp-item-name">${cmd.name}</span>
${cmd.meta ? `<span class="cp-item-meta">${cmd.meta}</span>` : ""}
</span>
${cmd.shortcut ? `<kbd class="cp-item-kbd">${cmd.shortcut}</kbd>` : ""}
`;
li.addEventListener("click", () => execute(cmd));
li.addEventListener("mouseenter", () => setActive(i));
list.appendChild(li);
});
}
// ── Filter ─────────────────────────────────────────────────────
function filter(query) {
const q = query.toLowerCase().trim();
activeIndex = -1;
if (!q) {
render(COMMANDS);
return;
}
const filtered = COMMANDS.filter(
(c) => c.name.toLowerCase().includes(q) || c.group.toLowerCase().includes(q)
);
render(filtered);
}
// ── Active item ────────────────────────────────────────────────
function setActive(index) {
const items = list.querySelectorAll(".cp-item");
items.forEach((el, i) => el.classList.toggle("active", i === index));
activeIndex = index;
}
function moveActive(delta) {
const items = list.querySelectorAll(".cp-item");
if (!items.length) return;
const next = Math.max(0, Math.min(items.length - 1, activeIndex + delta));
setActive(next);
items[next].scrollIntoView({ block: "nearest" });
}
// ── Execute ────────────────────────────────────────────────────
function execute(cmd) {
// Demo: just show which command was selected
alert(`"${cmd.name}" — action would run here`);
close();
}
// ── Events ────────────────────────────────────────────────────
// ⌘K / Ctrl+K
document.addEventListener("keydown", (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
overlay.hidden ? open() : close();
}
});
// Input filtering
input.addEventListener("input", () => filter(input.value));
// Keyboard navigation inside palette
input.addEventListener("keydown", (e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
moveActive(1);
}
if (e.key === "ArrowUp") {
e.preventDefault();
moveActive(-1);
}
if (e.key === "Escape") {
close();
}
if (e.key === "Enter") {
const items = list.querySelectorAll(".cp-item");
if (activeIndex >= 0 && items[activeIndex]) {
const cmdName = items[activeIndex].querySelector(".cp-item-name").textContent;
const cmd = COMMANDS.find((c) => c.name === cmdName);
if (cmd) execute(cmd);
}
}
});
// Click backdrop to close
overlay.addEventListener("click", (e) => {
if (e.target === overlay) close();
});
// Open immediately for demo
open();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Command Palette</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page-hint">
Press <kbd>⌘K</kbd> or <kbd>Ctrl+K</kbd> to open
</div>
<!-- Overlay -->
<div class="cp-overlay" id="cp-overlay" role="dialog" aria-modal="true" aria-label="Command palette" hidden>
<div class="cp-modal">
<div class="cp-search-wrap">
<svg class="cp-search-icon" aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
</svg>
<input
class="cp-input"
id="cp-input"
type="text"
placeholder="Search commands…"
autocomplete="off"
spellcheck="false"
aria-label="Search commands"
aria-autocomplete="list"
aria-controls="cp-list"
/>
<kbd class="cp-esc-hint">ESC</kbd>
</div>
<ul class="cp-list" id="cp-list" role="listbox">
<!-- populated by JS -->
</ul>
<div class="cp-footer">
<span><kbd>↑</kbd><kbd>↓</kbd> navigate</span>
<span><kbd>↵</kbd> select</span>
<span><kbd>ESC</kbd> close</span>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Command Palette (⌘K)
A fully accessible command palette modal triggered by ⌘K (Mac) or Ctrl+K (Windows/Linux). Features real-time fuzzy search, full keyboard navigation, and smooth enter/exit animations.
How it works
keydownlistener catches ⌘K / Ctrl+K and toggles the overlay- The search
<input>filters commands in real-time usingincludes()matching - Arrow keys move the active index;
Enterexecutes the highlighted command - Clicking the backdrop or pressing
Escapecloses the palette
Accessibility
role="dialog"+aria-modal="true"on the overlayaria-labelon the search input- Focus is trapped inside the modal while open
prefers-reduced-motionrespected for animations
When to use it
- App-wide navigation shortcut
- Quick action launcher
- Developer tools / power-user features