はじめに
最近のWebアプリ、⌘K(Ctrl+K)で検索メニューが開くやつ増えましたよね。NotionとかLinear、Vercelのダッシュボードなんかでもおなじみのアレです。
個人的にはこのUI、めちゃくちゃ好きなんですよ。マウスに手を伸ばさなくてもキーボードだけでサクサク操作できるし、検索もフィルタリングも一箇所で完結する。正直なところ、一度慣れると普通のナビゲーションには戻れなくなります。
で、これを自分のアプリにも導入したいと思って調べたら、Cmdkというライブラリに出会いました。これ、意外と簡単に実装できて、カスタマイズ性も高いので紹介します。
Cmdkとは
Cmdkは、React向けのコマンドメニューコンポーネントです。Vercelのデザイナー/エンジニアであるPaco Coursey氏が開発しています。
主な特徴をざっくり挙げると:
- 自動フィルタリング: 入力に応じてアイテムを自動でフィルタリング・ソート
- アクセシビリティ対応: WAI-ARIAパターンに準拠したアクセシブルなコンボボックス
- 完全にカスタマイズ可能: スタイルもロジックも自由に変更できる
- 軽量: gzip後で約5KB程度と軽い
- TypeScript対応: 型定義が完備されている
要するに、「見た目は自分で作れ、でもロジックは全部用意してやる」というスタンスのライブラリですね。
インストール方法
インストールは普通にnpmやpnpmで。
# npm
npm install cmdk
# pnpm
pnpm install cmdk
# yarn
yarn add cmdk
依存関係はReact 18以上が必要です。
基本的な使い方
最もシンプルな実装はこんな感じです。
import { Command } from 'cmdk'
function CommandMenu() {
return (
<Command>
<Command.Input placeholder="検索..." />
<Command.List>
<Command.Empty>見つかりませんでした</Command.Empty>
<Command.Group heading="ページ">
<Command.Item onSelect={() => console.log('Home')}>
ホーム
</Command.Item>
<Command.Item onSelect={() => console.log('About')}>
About
</Command.Item>
<Command.Item onSelect={() => console.log('Contact')}>
お問い合わせ
</Command.Item>
</Command.Group>
<Command.Group heading="アクション">
<Command.Item onSelect={() => console.log('Copy')}>
コピー
</Command.Item>
<Command.Item onSelect={() => console.log('Delete')}>
削除
</Command.Item>
</Command.Group>
</Command.List>
</Command>
)
}
これだけで、入力に応じたフィルタリングや、キーボードナビゲーション(上下キーでの移動、Enterで選択)が動作します。
ダイアログ形式での表示
実際のアプリでは、⌘Kで開閉するダイアログとして使うことが多いと思います。
import { Command } from 'cmdk'
import { useEffect, useState } from 'react'
function App() {
const [open, setOpen] = useState(false)
// ⌘K でトグル
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener('keydown', down)
return () => document.removeEventListener('keydown', down)
}, [])
return (
<Command.Dialog open={open} onOpenChange={setOpen}>
<Command.Input placeholder="コマンドを入力..." />
<Command.List>
<Command.Empty>見つかりません</Command.Empty>
<Command.Item>設定</Command.Item>
<Command.Item>プロフィール</Command.Item>
<Command.Item>ログアウト</Command.Item>
</Command.List>
</Command.Dialog>
)
}
Command.Dialogを使うと、オーバーレイ付きのモーダルとして表示されます。
主要なコンポーネント
Cmdkは以下のコンポーネントで構成されています。
| コンポーネント | 説明 |
|---|---|
Command |
ルート要素 |
Command.Dialog |
ダイアログ形式のラッパー |
Command.Input |
検索入力フィールド |
Command.List |
アイテムのコンテナ |
Command.Item |
選択可能なアイテム |
Command.Group |
アイテムのグループ化 |
Command.Separator |
区切り線 |
Command.Empty |
結果が空の時の表示 |
Command.Loading |
ローディング状態の表示 |
各コンポーネントにはdata-*属性が付与されるので、CSSでのスタイリングがしやすくなっています。
実践的なユースケース
カスタムフィルタリング
デフォルトのフィルタリングではなく、独自のロジックを使いたい場合。
<Command
filter={(value, search) => {
// 完全一致の場合のみ表示
if (value.includes(search)) return 1
return 0
}}
>
{/* ... */}
</Command>
キーワードによる検索
アイテムに別名(エイリアス)を設定して検索対象を広げられます。
<Command.Item keywords={['settings', '設定', 'config']}>
環境設定
</Command.Item>
これで「settings」と入力しても「環境設定」がヒットするようになります。
動的なアイテム読み込み
APIからデータを取得してアイテムを動的に生成するパターン。
function SearchCommand() {
const [loading, setLoading] = useState(false)
const [items, setItems] = useState<string[]>([])
const handleSearch = async (search: string) => {
setLoading(true)
const results = await fetchSearchResults(search)
setItems(results)
setLoading(false)
}
return (
<Command shouldFilter={false}>
<Command.Input onValueChange={handleSearch} />
<Command.List>
{loading && <Command.Loading>検索中...</Command.Loading>}
<Command.Empty>結果がありません</Command.Empty>
{items.map((item) => (
<Command.Item key={item}>{item}</Command.Item>
))}
</Command.List>
</Command>
)
}
shouldFilter={false}を指定することで、ビルトインのフィルタリングを無効化し、自前のロジックで制御できます。
スタイリング
Cmdkはスタイルを一切持っていないので、自分でCSSを書く必要があります。各コンポーネントには[cmdk-*]というデータ属性が付くので、それを使ってスタイリングします。
[cmdk-root] {
background: #1a1a1a;
border-radius: 12px;
overflow: hidden;
padding: 8px;
}
[cmdk-input] {
width: 100%;
padding: 12px 16px;
font-size: 16px;
background: transparent;
border: none;
color: #fff;
outline: none;
}
[cmdk-item] {
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
color: #999;
}
[cmdk-item][data-selected='true'] {
background: #333;
color: #fff;
}
Tailwind CSSを使う場合は、普通にclassNameを指定すればOKです。
まとめ
Cmdkは、コマンドメニューを実装するなら正直これ一択という感じのライブラリです。
良いところ:
- スタイルが含まれていないので、デザインの自由度が高い
- アクセシビリティがちゃんと考慮されている
- APIがシンプルで学習コストが低い
- TypeScriptとの相性が良い
注意点:
- スタイルは全部自分で書く必要がある
- Radix UIのDialogを内部で使っているので、そこは好み分かれるかも
30代になってから「時短」と「効率化」にこだわるようになった自分としては、このUIパターンは本当にQOL上がるんですよね。自分のプロダクトにも積極的に取り入れていきたいところです。
コマンドメニューを実装する機会があれば、ぜひCmdkを試してみてください。