command palette.
A keyboard-first command palette with fuzzy matching and one highlight that travels between rows.
live component, try it
what to expect.
how it works.
The half nobody handles is who owns the selection. A cursor resting over row three steals it the instant the list scrolls under it while you are arrowing, because the browser fires pointerover at a stationary cursor whenever the element beneath it changes, and a palette that loses your place mid-keystroke is worse than one with no mouse support at all. So hover is ignored until a real pointermove says the mouse actually moved, and every arrow key disarms it again. The highlight is the other half: one element that travels between rows rather than a class that jumps, placed without travelling on the first render so the palette never opens with a bar sliding in from nowhere.
the component.
import {
useEffect,
useLayoutEffect,
useRef,
useState,
type KeyboardEvent,
type CSSProperties,
} from "react";
import { match, nextIndex, type Ranges } from "./filter";
export interface Item {
id: string;
label: string;
hint: string;
}
export interface PaletteProps {
items: Item[];
max: number;
travel: number;
hoverSteals: boolean;
}
function Marked({ text, ranges }: { text: string; ranges: Ranges }) {
if (ranges.length === 0) return <>{text}</>;
const out = [];
let at = 0;
for (const [start, end] of ranges) {
if (start > at) out.push(text.slice(at, start));
out.push(<mark key={start}>{text.slice(start, end)}</mark>);
at = end;
}
if (at < text.length) out.push(text.slice(at));
return <>{out}</>;
}
export default function CommandPalette({ items, max, travel, hoverSteals }: PaletteProps) {
const [query, setQuery] = useState("");
const [selected, setSelected] = useState(0);
const listRef = useRef<HTMLUListElement>(null);
const barRef = useRef<HTMLDivElement>(null);
const hoverArmed = useRef(false);
const armed = useRef(false);
const [armedState, setArmedState] = useState(false);
const results = items
.map((item) => ({ item, ranges: match(query, item.label) }))
.filter((r): r is { item: Item; ranges: Ranges } => r.ranges !== null)
.slice(0, max);
useEffect(() => {
setSelected(0);
}, [query]);
useLayoutEffect(() => {
const row = listRef.current?.children[selected] as HTMLElement | undefined;
const bar = barRef.current;
if (!row || !bar) return;
bar.style.transform = `translateY(${row.offsetTop}px)`;
bar.style.height = `${row.offsetHeight}px`;
const wrap = row.closest(".cp-list-wrap") as HTMLElement | null;
if (wrap) {
const top = row.offsetTop;
const bottom = top + row.offsetHeight;
if (top < wrap.scrollTop) wrap.scrollTop = row.previousElementSibling ? top : 0;
else if (bottom > wrap.scrollTop + wrap.clientHeight) wrap.scrollTop = bottom - wrap.clientHeight;
}
// Place the highlight without animating on first render, then enable travel.
if (!armed.current) {
armed.current = true;
requestAnimationFrame(() => setArmedState(true));
}
}, [selected, results.length]);
const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
e.preventDefault();
// Arrow keys own the selection until the mouse actually moves again.
hoverArmed.current = false;
setSelected((s) => nextIndex(s, e.key === "ArrowDown" ? 1 : -1, results.length));
};
const activeId = results[selected] ? `cp-${results[selected]!.item.id}` : undefined;
return (
<div
className="cp"
onPointerMove={() => {
// Only a real pointermove re-enables hover; scrolling fires pointerover at a still cursor.
hoverArmed.current = true;
}}
>
<input
className="cp-input"
type="text"
role="combobox"
aria-expanded="true"
aria-controls="cp-list"
aria-activedescendant={activeId}
aria-label="search"
placeholder="type a command"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={onKeyDown}
/>
<div className="cp-list-wrap" style={{ "--cp-travel": `${travel}ms` } as CSSProperties}>
<div className="cp-bar" ref={barRef} data-armed={String(armedState)} aria-hidden="true" />
<ul className="cp-list" id="cp-list" role="listbox" aria-label="results" ref={listRef}>
{results.map(({ item, ranges }, i) => (
<li
key={item.id}
id={`cp-${item.id}`}
role="option"
aria-selected={i === selected}
className="cp-row"
data-on={String(i === selected)}
onPointerEnter={() => {
if (hoverSteals || hoverArmed.current) setSelected(i);
}}
>
<span>
<Marked text={item.label} ranges={ranges} />
</span>
<span className="cp-hint">{item.hint}</span>
</li>
))}
{results.length === 0 && <li className="cp-empty">nothing matches.</li>}
</ul>
</div>
</div>
);
}