コマンドパレット
ボタンまたは ⌘K/Ctrl+K でコマンド検索オーバーレイを開き、入力で絞り込み、↑↓キーまたはタップで実行できる。
プレビュー
操作クリック/タップで動きます。
プレビューは枠内に収めるため、オーバーレイを position:absolute(ステージ基準)で実装しています。実際のページで画面全体に重ねるときは position:fixed に変え、body 直下など高い階層へ置いてください。
使いどころ
操作対象が多く、機能を探すより名前を思い出して直接呼び出したい熟練ユーザー向けのSaaSダッシュボードやエディタ、ドキュメントツールに向く。⌘K/Ctrl+Kという慣習化されたショートカットで、メニューの階層をたどらずに目的の操作へ一直線に行ける速さが価値になる。逆に、操作項目が10個に満たないような小規模なアプリでは、コマンド一覧を保守するコストに見合わず、通常のメニューやツールバーで十分。また初回利用者や非熟練ユーザーが主対象の画面にも不向きで、⌘Kの存在に気づいてもらうこと自体が課題になる。サンプルのサブタイトルにある「⌘Kでコマンドを開く」のような明示的なヒントを画面に常設し、開くボタンも併設しておくことで、ショートカットを知らない利用者への導線を切らさないようにする必要がある。
仕組み
document全体にkeydownリスナーを1つ張り、metaKey(Mac)またはctrlKey(Windows/Linux)と'k'の組み合わせを検知してオーバーレイの開閉をトグルする。フォーカスがどこにあっても発火するグローバル方式のため、他の入力欄にフォーカスがある状態でもショートカットが効いてしまう点は設計として意識しておく必要がある。絞り込みは入力のたびにCOMMANDS配列をlabelの部分一致(小文字化したincludes)でフィルタし、リストをまるごと作り直す単純な実装で、10件程度の規模なら問題にならない。フィルタ後は常に先頭の項目を自動でアクティブにするため、入力してすぐEnterを押すだけで矢印キーを使わずに最有力候補を選べる。↑↓キーでのアクティブ切り替えはMath.min/maxで端に達したら止まるクランプ方式で、端から反対側へ循環はしない。
実装の注意
このサンプルのEnterキーとリスト項目クリックのハンドラはcloseを呼ぶだけで、選ばれたコマンドを実際に実行する処理を持たない。実装するときは各コマンド定義に実行関数を持たせ、閉じる前後にそれを呼び出す形にする必要がある。次に、document全体で⌘K/Ctrl+Kを奪うグローバル方式は、他のテキスト入力中や別の機能が同じショートカットを使っている場面と衝突しうる。意図して全域で奪うのか、入力欄にフォーカスがあるときは無視するのかは設計として決めておく必要がある。また↑↓キーが端で止まり循環しない挙動は、循環する実装に慣れた利用者には違和感が出ることがあるため、意図的な選択として決めておくべきである。加えて、開くたびにinput.focusは呼んでいるが、閉じるときに元々フォーカスしていた要素(開くボタン等)へフォーカスを戻す処理が無く、閉じた後にフォーカスが行方不明になりやすい。
アクセシビリティ
オーバーレイにはrole=dialog aria-modal=trueが付いているが、Tabキーの移動をパレット内に閉じ込めるフォーカストラップのJSは実装されていない。aria-modal=trueは背後を支援技術から隠す指示だが、視覚的なTab移動は制御されておらず、開いたままTabを押すと背後の要素へフォーカスが抜けてしまう。次に、リスト項目はrole=optionを持つがaria-selectedが無く、各liにIDも振られていない。入力欄側にもrole=combobox aria-expanded aria-controls aria-activedescendantが無いため、↑↓キーでどの項目がアクティブか(視覚的には背景色の変化で分かる)がスクリーンリーダーには伝わらない。WAI-ARIAのコンボボックス+リストボックスパターンに沿うなら、各liに安定したIDを振り、inputにaria-activedescendantでそのIDを同期させ、アクティブな項目にaria-selected=trueを付ける実装が要る。加えて閉じたときに開くボタンへフォーカスを戻す処理も、支援技術利用者が元の文脈に戻るために必要になる。
調整して使う
空の入力状態では「最近使ったコマンド」や「よく使うコマンド」を先に並べておくと、検索語を思いつく前から使い始められる。各項目の右端にキーボードショートカットのkbdタグを添えると、パレット自体を使わずに直接そのキーを覚えて呼び出す動線にもなる。部分一致だけでなく、タイプミスや語順の違いを許容するファジーマッチに強化すると、正確な名称を覚えていなくても見つけやすくなる。項目数が増えたらカテゴリ見出しで区切ったり、まず大分類を選んでから絞り込む階層型(ドリルダウン式)に発展させる設計もある。実行頻度や最終使用日時でソート順を動的に変えると、使うほど目的の項目が上位に来るようになり、10項目を超える規模でも探す時間を短縮できる。
コード(コピーして使えます)
<div class="cp-wrap" id="cp-wrap">
<div class="cp-app">
<p class="cp-app-title">ダッシュボード</p>
<p class="cp-app-sub">⌘K でコマンドを開く</p>
<button class="cp-open-btn" id="cp-open-btn">コマンドを開く</button>
</div>
<div class="cp-overlay hidden" id="cp-overlay" role="dialog" aria-modal="true" aria-label="コマンドパレット">
<div class="cp-palette">
<div class="cp-search-row">
<span class="cp-search-icon">⌕</span>
<input class="cp-input" id="cp-input" type="text" placeholder="コマンドを検索…" autocomplete="off" />
<kbd class="cp-esc">Esc</kbd>
</div>
<ul class="cp-list" id="cp-list" role="listbox"></ul>
</div>
</div>
</div>