コマンドパレット
ルール
cmdk ライブラリを使用 — 合成可能、アクセシブル、高速なファジー検索が組み込み済み
- トリガー:
Cmd+K(Mac)/ Ctrl+K(Windows/Linux) — グローバルキーボードリスナーを登録
- アクションのグループ化: ナビゲーション(ページ)、コマンド(アクション)、最近使った項目(最終使用) — グループ見出しを表示
- キーボードナビゲーション: 矢印キーで移動、Enterで選択、Escapeで閉じる — すべてcmdkが処理
- ファジー検索: cmdkが自動フィルタリング — スコアベースのランキングによる一致強調表示
- 最近使った項目: 最後に選択した5〜10個の項目をlocalStorageに保存 — 入力が空のときは上部に表示
- 動的アクション: アプリのさまざまな部分からアクションを登録 — コンテキスト認識(異なるページで異なるアクションを表示)
- ローディング状態: 非同期検索結果(例: API検索) — 結果領域にローディングインジケーターを表示
パターン
import { Command } from "cmdk";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
function CommandPalette() {
const [open, setOpen] = useState(false);
const router = useRouter();
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((prev) => !prev);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
return (
<Command.Dialog open={open} onOpenChange={setOpen} label="コマンドパレット">
<Command.Input placeholder="コマンドを検索または入力..." />
<Command.List>
<Command.Empty>結果が見つかりません。</Command.Empty>
<Command.Group heading="最近使った項目">
{recentItems.map((item) => (
<Command.Item key={item.id} onSelect={() => { router.push(item.href); setOpen(false); }}>
{item.label}
</Command.Item>
))}
</Command.Group>
<Command.Group heading="ナビゲーション">
<Command.Item onSelect={() => { router.push("/dashboard"); setOpen(false); }}>
ダッシュボード
</Command.Item>
<Command.Item onSelect={() => { router.push("/settings"); setOpen(false); }}>
設定
</Command.Item>
</Command.Group>
<Command.Group heading="コマンド">
<Command.Item onSelect={() => { toggleTheme(); setOpen(false); }}>
ダークモードを切り替え
</Command.Item>
<Command.Item onSelect={() => { logout(); setOpen(false); }}>
サインアウト
</Command.Item>
</Command.Group>
</Command.List>
</Command.Dialog>
);
}
避けるべきこと
- カスタムファジー検索を構築しない — cmdkはフィルタリング、スコアリング、ランキングを標準で処理します
Cmd+K で e.preventDefault() を忘れない — 忘れるとブラウザのデフォルト検索/アドレスバーが開きます
- Escapeキーの処理を省略しない — cmdkは
onOpenChange でこれを処理しますが、フォーカスが前の要素に戻ることを確認してください
- 静的なアクションのみにしない — ページコンテキストから動的にアクションを登録してください(例: 「この投稿を編集」は投稿ページでのみ表示)
- 空の状態を表示しない — 検索に一致するものが何もない場合は必ず「結果が見つかりません」と表示してください