Command
Command menu for search and quick actions.
Demo
Dependencies
lucide-reactclsxtailwind-merge
Props
This is a base primitive. It accepts standard HTML attributes plus className for styling via cn().
Source
"use client";
import { useState, useRef, useEffect, useCallback } from "react";
import { cn } from "@/lib/utils";
import { Search } from "lucide-react";
export interface CommandItem {
id: string;
label: string;
keywords?: string[];
onSelect?: () => void;
}
export interface CommandProps {
items: CommandItem[];
placeholder?: string;
emptyText?: string;
className?: string;
}
export function Command({
items,
placeholder = "Type a command...",
emptyText = "No results.",
className,
}: CommandProps) {
const [query, setQuery] = useState("");
const [selectedIndex, setSelectedIndex] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const filtered = query
? items.filter((item) => {
const q = query.toLowerCase();
return (
item.label.toLowerCase().includes(q) ||
item.keywords?.some((k) => k.toLowerCase().includes(q))
);
})
: items;
useEffect(() => {
setSelectedIndex(0);
}, [query]);
useEffect(() => {
inputRef.current?.focus();
}, []);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedIndex((i) => Math.min(i + 1, filtered.length - 1));
}
if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedIndex((i) => Math.max(i - 1, 0));
}
if (e.key === "Enter" && filtered[selectedIndex]) {
filtered[selectedIndex].onSelect?.();
}
},
[filtered, selectedIndex],
);
return (
<div
className={cn(
"rounded-lg border border-gray-200 bg-white shadow-sm",
className,
)}
>
<div className="flex items-center border-b border-gray-200 px-3">
<Search className="h-4 w-4 shrink-0 text-gray-400" />
<input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className="ml-2 flex-1 border-0 bg-transparent py-3 text-sm outline-none"
/>
</div>
<div className="max-h-64 overflow-y-auto py-1">
{filtered.length === 0 ? (
<p className="px-3 py-6 text-center text-sm text-gray-400">
{emptyText}
</p>
) : (
filtered.map((item, i) => (
<button
key={item.id}
onClick={item.onSelect}
className={cn(
"flex w-full items-center px-3 py-2 text-left text-sm transition-colors",
i === selectedIndex
? "bg-gray-100 text-gray-900"
: "text-gray-700 hover:bg-gray-50",
)}
>
{item.label}
</button>
))
)}
</div>
</div>
);
}
CLI
npx genui add command