useCommandK Palette Hook

The cmd-k palette hook I wire into every internal tool. Owns global keyboard shortcuts, fuzzy match, arrow-key navigation, and a focus trap, with no external dependencies.

JavaScript
Frontend
3 snippets
react
hooks
accessibility
command-palette
zurihayes

By @zurihayes

January 21, 2026

·

Updated May 18, 2026

1,168 views

6

4.4 (13)

// Subsequence-style fuzzy match: lowercase the haystack, walk both strings,
// score by run-length and word-boundary bonuses. Easy to test, easy to tune.

function fuzzyScore(haystack, needle) {
    if (!needle) return { ok: true, score: 0, matches: [] };
    const h = haystack.toLowerCase();
    const n = needle.toLowerCase();
    let i = 0, j = 0, score = 0, run = 0;
    const matches = [];
    while (i < h.length && j < n.length) {
        if (h[i] === n[j]) {
            matches.push(i);
            run++;
            score += run;
            if (i === 0 || h[i - 1] === ' ' || h[i - 1] === '/') score += 2;
            j++;
        } else {
            run = 0;
        }
        i++;
    }
    if (j < n.length) return { ok: false, score: 0, matches: [] };
    return { ok: true, score, matches };
}

function rank(commands, query) {
    return commands
        .map((c) => ({ cmd: c, ...fuzzyScore(c.label, query) }))
        .filter((r) => r.ok)
        .sort((a, b) => b.score - a.score);
}

const commands = [
    { id: 'new-doc', label: 'New Document' },
    { id: 'open-settings', label: 'Open Settings' },
    { id: 'invite', label: 'Invite Teammate' },
    { id: 'theme', label: 'Toggle Dark Theme' },
];

console.log('query="new"     ->', rank(commands, 'new').map((r) => r.cmd.label));
console.log('query="settings"->', rank(commands, 'settings').map((r) => r.cmd.label));
console.log('query="tdt"     ->', rank(commands, 'tdt').map((r) => r.cmd.label));

I never reach for fuse.js for a command palette anymore: a 25-line fuzzy matcher is enough and ranks better for the shape of palette commands. Three things make it work: subsequence matching means tdt finds Toggle Dark Theme; run-length scoring means contiguous matches outscore scattered ones; and a small bonus for word-boundary matches surfaces Settings when you type set. Keeping the function pure means I can sort 200 commands at 60fps in JS without a single render, and I can table-test it with five lines of vitest. The matches array is what I pass back to highlight characters in the rendered list.