shadcn/ui で Command Palette を実装:検索・履歴・キーボードショートカット対応

モダンな Web アプリケーションで欠かせなくなった Command Palette(コマンドパレット)。VS Code や GitHub など、多くのアプリケーションが採用しており、ユーザーは Ctrl+K
や Cmd+K
を押すだけで、すべての機能に素早くアクセスできます。この記事では、shadcn/ui を使って、検索・履歴・キーボードショートカット対応の本格的な Command Palette を実装する方法を解説します。
初心者の方でも安心して実装できるよう、段階的に進めていきますので、ぜひ最後までお付き合いください。
背景
Command Palette が必要とされる理由
現代の Web アプリケーションは多機能化が進み、ユーザーが利用できる機能も増えています。しかし、機能が増えるほど「どこにその機能があるのか」を探すのが難しくなってしまいます。
Command Palette は、この課題を解決する UI パターンの一つです。ユーザーはキーボードショートカットでパレットを開き、実行したい操作を検索できるため、マウス操作よりも圧倒的に効率的なんです。
shadcn/ui の魅力
shadcn/ui は、コピー&ペーストで使えるコンポーネントライブラリとして人気を集めています。npm パッケージとしてインストールするのではなく、必要なコンポーネントだけをプロジェクトにコピーして使うため、カスタマイズが容易で、バンドルサイズも最小限に抑えられます。
Command Palette の実装には、shadcn/ui の Command
コンポーネントと Dialog
コンポーネントを組み合わせて使用します。
以下の図は、Command Palette がアプリケーションのどの層に位置するかを示しています。
mermaidflowchart TB
user["ユーザー"]
ui["UI Layer<br/>(shadcn/ui コンポーネント)"]
palette["Command Palette<br/>(Dialog + Command)"]
action["アクション実行<br/>(ナビゲーション・設定など)"]
user -->|"Cmd+K / Ctrl+K"| palette
palette -.->|"コンポーネント"| ui
palette -->|"コマンド選択"| action
action -->|"フィードバック"| user
この図からわかるように、Command Palette はユーザーとアクション実行の間をつなぐ重要な役割を担っています。
実装する機能の全体像
本記事で実装する Command Palette には、以下の機能を含めます。
# | 機能 | 説明 |
---|---|---|
1 | キーボードショートカット | Cmd+K (Mac)/ Ctrl+K (Windows)で開閉 |
2 | リアルタイム検索 | 入力に応じてコマンドを即座にフィルタリング |
3 | コマンド履歴 | 過去に実行したコマンドを記録・表示 |
4 | グループ化 | コマンドをカテゴリー別に整理 |
5 | キーボードナビゲーション | ↑↓ キーで選択、Enter で実行 |
課題
既存の実装方法の問題点
Command Palette を一から実装しようとすると、以下のような課題に直面します。
アクセシビリティ対応の複雑さ
キーボード操作やスクリーンリーダー対応など、アクセシビリティを考慮した実装は非常に複雑です。適切な ARIA 属性の設定、フォーカス管理、キーボードイベントのハンドリングなど、考慮すべき点が多岐にわたります。
状態管理の煩雑さ
Command Palette には、開閉状態、検索クエリ、選択中のアイテム、コマンド履歴など、複数の状態を管理する必要があります。これらを適切に管理しないと、バグの温床になってしまいます。
スタイリングの一貫性
アプリケーション全体のデザインシステムと一貫性を保ちながら、使いやすい UI を実装するのは簡単ではありません。
以下の図は、Command Palette 実装時に考慮すべき課題を整理したものです。
mermaidflowchart TD
impl["Command Palette<br/>実装の課題"]
a11y["アクセシビリティ"]
state["状態管理"]
style_["スタイリング"]
perf["パフォーマンス"]
a11y_detail["・ARIA 属性<br/>・フォーカス管理<br/>・キーボード操作"]
state_detail["・開閉状態<br/>・検索クエリ<br/>・履歴管理"]
style_detail["・デザイン一貫性<br/>・レスポンシブ<br/>・アニメーション"]
perf_detail["・検索の高速化<br/>・レンダリング最適化<br/>・遅延読み込み"]
impl --> a11y
impl --> state
impl --> style_
impl --> perf
a11y --> a11y_detail
state --> state_detail
style_ --> style_detail
perf --> perf_detail
これらの課題をすべて自力で解決するのは、時間とコストがかかりすぎてしまいます。
shadcn/ui による課題解決
shadcn/ui を使えば、これらの課題の多くが解決されます。
- アクセシビリティ: Radix UI をベースにしているため、標準で対応済み
- 状態管理: React のフック API で簡潔に記述可能
- スタイリング: Tailwind CSS による一貫したデザイン
- パフォーマンス: cmdk ライブラリによる高速な検索
解決策
環境構築とパッケージインストール
まず、Next.js プロジェクトに shadcn/ui をセットアップします。すでに Next.js プロジェクトがある前提で進めますが、新規プロジェクトの場合は yarn create next-app
から始めてください。
shadcn/ui の初期化
shadcn/ui CLI を使って、プロジェクトを初期化します。この CLI は、必要な設定ファイルと依存関係を自動的にセットアップしてくれます。
bashyarn add -D @shadcn/ui
yarn shadcn-ui init
初期化時に、以下の質問に答えます。
- スタイル: Default(デフォルトのまま推奨)
- ベースカラー: お好みで選択(Slate がおすすめ)
- CSS 変数: Yes(カスタマイズしやすくなります)
必要なコンポーネントの追加
Command Palette に必要な command
と dialog
コンポーネントを追加しましょう。
bashyarn shadcn-ui add command dialog
このコマンドを実行すると、components/ui/command.tsx
と components/ui/dialog.tsx
がプロジェクトに追加されます。
基本的な Command Palette の実装
それでは、基本的な Command Palette を実装していきます。
コンポーネントの構造設計
Command Palette は、以下の階層で構成します。
- Dialog: モーダル表示を担当
- Command: コマンド検索と選択を担当
- CommandInput: 検索入力フィールド
- CommandList: コマンドのリスト表示
- CommandGroup: コマンドのグループ化
- CommandItem: 個別のコマンド
以下は、これらのコンポーネントの関係性を示した図です。
mermaidflowchart TB
Dialog["Dialog<br/>(モーダル表示)"]
Command["Command<br/>(検索・選択機能)"]
Input["CommandInput<br/>(検索入力)"]
List["CommandList<br/>(リスト表示)"]
Group1["CommandGroup<br/>(ナビゲーション)"]
Group2["CommandGroup<br/>(設定)"]
Item1["CommandItem"]
Item2["CommandItem"]
Item3["CommandItem"]
Dialog --> Command
Command --> Input
Command --> List
List --> Group1
List --> Group2
Group1 --> Item1
Group1 --> Item2
Group2 --> Item3
この構造により、役割が明確に分離され、保守性の高いコードになります。
CommandPalette コンポーネントの作成
まず、基本的な CommandPalette コンポーネントを作成します。このコンポーネントは、Dialog と Command を組み合わせて、モーダル形式で表示します。
typescript'use client';
import * as React from 'react';
import {
Dialog,
DialogContent,
} from '@/components/ui/dialog';
import {
Command,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
} from '@/components/ui/command';
次に、コンポーネントの基本的な型定義を行います。
typescript// コマンドアイテムの型定義
interface CommandItemType {
id: string;
label: string;
icon?: React.ReactNode;
onSelect: () => void;
keywords?: string[]; // 検索用キーワード
}
// コマンドグループの型定義
interface CommandGroupType {
heading: string;
items: CommandItemType[];
}
// Props の型定義
interface CommandPaletteProps {
groups: CommandGroupType[];
}
型定義により、TypeScript の補完機能が使えるようになり、開発効率が向上します。
開閉状態の管理
Command Palette の開閉状態を React の useState
で管理します。また、useEffect
を使ってキーボードショートカットを設定しましょう。
typescriptexport function CommandPalette({ groups }: CommandPaletteProps) {
const [open, setOpen] = React.useState(false)
// Cmd+K / Ctrl+K でパレットを開閉
React.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)
}, [])
この実装により、ユーザーは Cmd+K
(Mac)または Ctrl+K
(Windows)を押すだけで、いつでも Command Palette を呼び出せます。
UI のレンダリング
Dialog と Command を組み合わせて、UI をレンダリングします。
typescript return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground">
<CommandInput placeholder="コマンドを検索..." />
<CommandList>
<CommandEmpty>結果が見つかりませんでした。</CommandEmpty>
次に、各グループとアイテムをレンダリングします。
typescript {groups.map((group) => (
<CommandGroup key={group.heading} heading={group.heading}>
{group.items.map((item) => (
<CommandItem
key={item.id}
onSelect={() => {
item.onSelect()
setOpen(false) // コマンド実行後に閉じる
}}
keywords={item.keywords}
>
{item.icon && (
<span className="mr-2">{item.icon}</span>
)}
<span>{item.label}</span>
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</DialogContent>
</Dialog>
)
}
これで、基本的な Command Palette が完成しました。
キーボードショートカットの拡張
より使いやすくするために、個別のコマンドにもキーボードショートカットを割り当てましょう。
ショートカット表示の追加
まず、コマンドアイテムの型定義に shortcut
プロパティを追加します。
typescriptinterface CommandItemType {
id: string;
label: string;
icon?: React.ReactNode;
onSelect: () => void;
keywords?: string[];
shortcut?: string; // ショートカットキー(例: "⌘P", "Ctrl+P")
}
次に、CommandItem のレンダリング部分を更新します。
typescript<CommandItem
key={item.id}
onSelect={() => {
item.onSelect();
setOpen(false);
}}
keywords={item.keywords}
>
<div className='flex items-center gap-2 flex-1'>
{item.icon && <span className='mr-2'>{item.icon}</span>}
<span>{item.label}</span>
</div>
{item.shortcut && (
<kbd className='pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100'>
{item.shortcut}
</kbd>
)}
</CommandItem>
これで、各コマンドにショートカットキーが表示されるようになります。
グローバルショートカットの実装
個別のコマンドをグローバルショートカットで実行できるようにします。
typescriptexport function CommandPalette({ groups }: CommandPaletteProps) {
const [open, setOpen] = React.useState(false)
// すべてのコマンドを平坦化してマッピング
const commandMap = React.useMemo(() => {
const map = new Map<string, CommandItemType>()
groups.forEach((group) => {
group.items.forEach((item) => {
if (item.shortcut) {
map.set(item.shortcut, item)
}
})
})
return map
}, [groups])
キーボードイベントリスナーを拡張して、個別のショートカットにも対応します。
typescriptReact.useEffect(() => {
const down = (e: KeyboardEvent) => {
// Cmd+K / Ctrl+K でパレットを開閉
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
return;
}
// 個別のショートカット処理
const modifier = e.metaKey
? '⌘'
: e.ctrlKey
? 'Ctrl+'
: '';
const key = e.key.toUpperCase();
const shortcut = `${modifier}${key}`;
const command = commandMap.get(shortcut);
if (command && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
command.onSelect();
}
};
document.addEventListener('keydown', down);
return () =>
document.removeEventListener('keydown', down);
}, [commandMap]);
この実装により、Command Palette を開かなくても、直接ショートカットでコマンドを実行できるようになります。
コマンド履歴機能の実装
ユーザーが過去に実行したコマンドを記録し、再度実行しやすくする機能を追加しましょう。
履歴管理用カスタムフックの作成
履歴の保存と読み込みを行う useCommandHistory
フックを作成します。localStorage を使って、ブラウザを閉じても履歴が残るようにします。
typescriptimport * as React from "react"
interface HistoryItem {
id: string
label: string
timestamp: number
}
const STORAGE_KEY = "command-palette-history"
const MAX_HISTORY = 5 // 最大履歴数
export function useCommandHistory() {
const [history, setHistory] = React.useState<HistoryItem[]>([])
// 初回マウント時に localStorage から履歴を読み込み
React.useEffect(() => {
const saved = localStorage.getItem(STORAGE_KEY)
if (saved) {
try {
setHistory(JSON.parse(saved))
} catch (e) {
console.error("履歴の読み込みに失敗しました", e)
}
}
}, [])
履歴に新しいアイテムを追加する関数を実装します。
typescriptconst addToHistory = React.useCallback(
(item: Omit<HistoryItem, 'timestamp'>) => {
setHistory((prev) => {
// 同じコマンドが既に履歴にある場合は削除
const filtered = prev.filter((h) => h.id !== item.id);
// 新しいアイテムを先頭に追加
const newHistory = [
{ ...item, timestamp: Date.now() },
...filtered,
].slice(0, MAX_HISTORY); // 最大数を超えた分は削除
// localStorage に保存
localStorage.setItem(
STORAGE_KEY,
JSON.stringify(newHistory)
);
return newHistory;
});
},
[]
);
履歴をクリアする関数も追加します。
typescript const clearHistory = React.useCallback(() => {
setHistory([])
localStorage.removeItem(STORAGE_KEY)
}, [])
return { history, addToHistory, clearHistory }
}
このフックにより、履歴の管理が簡潔に記述できます。
CommandPalette コンポーネントへの統合
作成した useCommandHistory
フックを CommandPalette コンポーネントに統合します。
typescriptexport function CommandPalette({ groups }: CommandPaletteProps) {
const [open, setOpen] = React.useState(false)
const { history, addToHistory, clearHistory } = useCommandHistory()
// コマンド実行時に履歴に追加
const handleCommandSelect = (item: CommandItemType) => {
item.onSelect()
addToHistory({ id: item.id, label: item.label })
setOpen(false)
}
履歴グループを作成し、レンダリング部分に追加します。
typescript // 履歴グループを作成
const historyGroup: CommandGroupType | null = history.length > 0 ? {
heading: "最近使ったコマンド",
items: history.map((h) => {
// 元のコマンドを検索
const originalItem = groups
.flatMap(g => g.items)
.find(item => item.id === h.id)
return originalItem || {
id: h.id,
label: h.label,
onSelect: () => {}, // 元のコマンドが見つからない場合
}
}),
} : null
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="overflow-hidden p-0">
<Command>
<CommandInput placeholder="コマンドを検索..." />
<CommandList>
<CommandEmpty>結果が見つかりませんでした。</CommandEmpty>
{/* 履歴グループを最初に表示 */}
{historyGroup && (
<CommandGroup heading={historyGroup.heading}>
{historyGroup.items.map((item) => (
<CommandItem
key={`history-${item.id}`}
onSelect={() => handleCommandSelect(item)}
>
{item.icon && <span className="mr-2">{item.icon}</span>}
<span>{item.label}</span>
</CommandItem>
))}
</CommandGroup>
)}
通常のコマンドグループもレンダリングします。
typescript {/* 通常のコマンドグループ */}
{groups.map((group) => (
<CommandGroup key={group.heading} heading={group.heading}>
{group.items.map((item) => (
<CommandItem
key={item.id}
onSelect={() => handleCommandSelect(item)}
keywords={item.keywords}
>
<div className="flex items-center gap-2 flex-1">
{item.icon && <span className="mr-2">{item.icon}</span>}
<span>{item.label}</span>
</div>
{item.shortcut && (
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
{item.shortcut}
</kbd>
)}
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</DialogContent>
</Dialog>
)
}
これで、ユーザーが頻繁に使うコマンドにすぐアクセスできるようになりました。
具体例
実際のアプリケーションでの使用例
それでは、実際に CommandPalette コンポーネントを使ってみましょう。Next.js のページコンポーネントで使用する例を紹介します。
アイコンライブラリの追加
まず、コマンドにアイコンを表示するために、lucide-react をインストールします。
bashyarn add lucide-react
lucide-react は、shadcn/ui と相性の良いアイコンライブラリで、軽量かつモダンなアイコンが豊富に揃っています。
ページコンポーネントでの実装
Next.js の App Router を使った実装例です。
typescript'use client';
import { CommandPalette } from '@/components/command-palette';
import { useRouter } from 'next/navigation';
import {
Home,
Search,
Settings,
User,
FileText,
Mail,
Bell,
} from 'lucide-react';
次に、コマンドグループを定義します。
typescriptexport default function HomePage() {
const router = useRouter()
// コマンドグループの定義
const commandGroups = [
{
heading: "ナビゲーション",
items: [
{
id: "home",
label: "ホーム",
icon: <Home className="w-4 h-4" />,
onSelect: () => router.push("/"),
shortcut: "⌘H",
keywords: ["home", "ホーム", "トップ"],
},
{
id: "search",
label: "検索",
icon: <Search className="w-4 h-4" />,
onSelect: () => router.push("/search"),
shortcut: "⌘S",
keywords: ["search", "検索", "探す"],
},
{
id: "profile",
label: "プロフィール",
icon: <User className="w-4 h-4" />,
onSelect: () => router.push("/profile"),
keywords: ["profile", "プロフィール", "ユーザー"],
},
],
},
設定や通知に関するコマンドグループも追加します。
typescript {
heading: "設定",
items: [
{
id: "settings",
label: "設定",
icon: <Settings className="w-4 h-4" />,
onSelect: () => router.push("/settings"),
shortcut: "⌘,",
keywords: ["settings", "設定", "環境設定"],
},
{
id: "notifications",
label: "通知",
icon: <Bell className="w-4 h-4" />,
onSelect: () => router.push("/notifications"),
keywords: ["notifications", "通知", "お知らせ"],
},
],
},
アクション系のコマンドグループも定義します。
typescript {
heading: "アクション",
items: [
{
id: "new-post",
label: "新規投稿",
icon: <FileText className="w-4 h-4" />,
onSelect: () => router.push("/posts/new"),
shortcut: "⌘N",
keywords: ["new", "post", "新規", "投稿", "作成"],
},
{
id: "send-message",
label: "メッセージ送信",
icon: <Mail className="w-4 h-4" />,
onSelect: () => router.push("/messages/new"),
shortcut: "⌘M",
keywords: ["message", "メッセージ", "送信", "mail"],
},
],
},
]
return (
<div>
<CommandPalette groups={commandGroups} />
{/* ページの他のコンテンツ */}
<main className="container mx-auto p-8">
<h1 className="text-4xl font-bold mb-4">Welcome to My App</h1>
<p className="text-muted-foreground">
Cmd+K(または Ctrl+K)を押して、Command Palette を開いてください。
</p>
</main>
</div>
)
}
この実装により、ユーザーはキーボードショートカットで素早くアプリケーション内を移動できます。
動作フローの可視化
以下の図は、ユーザーが Command Palette を操作する際の流れを示しています。
mermaidsequenceDiagram
participant User as ユーザー
participant KB as キーボード
participant CP as CommandPalette
participant History as 履歴管理
participant Action as アクション実行
User->>KB: Cmd+K 押下
KB->>CP: パレット表示
CP->>History: 履歴読み込み
History-->>CP: 過去のコマンド
CP->>User: UI 表示(履歴 + 全コマンド)
User->>CP: 検索キーワード入力
CP->>CP: フィルタリング
CP->>User: 絞り込み結果表示
User->>CP: コマンド選択(Enter)
CP->>Action: onSelect 実行
CP->>History: 履歴に追加
History->>History: localStorage 保存
CP->>User: パレット非表示
Action->>User: アクション完了通知
このフローにより、ユーザーは直感的かつ効率的にコマンドを実行できます。
テーマ切り替えコマンドの追加例
shadcn/ui は next-themes と組み合わせて使うことが多いので、テーマ切り替えのコマンドも追加してみましょう。
まず、next-themes をインストールします。
bashyarn add next-themes
次に、テーマプロバイダーをセットアップします(layout.tsx)。
typescriptimport { ThemeProvider } from 'next-themes';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang='ja' suppressHydrationWarning>
<body>
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
);
}
ページコンポーネントにテーマ切り替えコマンドを追加します。
typescript"use client"
import { useTheme } from "next-themes"
import { Moon, Sun, Monitor } from "lucide-react"
export default function HomePage() {
const router = useRouter()
const { setTheme } = useTheme()
const commandGroups = [
// 既存のグループ...
{
heading: "外観",
items: [
{
id: "theme-light",
label: "ライトモード",
icon: <Sun className="w-4 h-4" />,
onSelect: () => setTheme("light"),
keywords: ["light", "ライト", "明るい", "white"],
},
{
id: "theme-dark",
label: "ダークモード",
icon: <Moon className="w-4 h-4" />,
onSelect: () => setTheme("dark"),
keywords: ["dark", "ダーク", "暗い", "black"],
},
{
id: "theme-system",
label: "システム設定に従う",
icon: <Monitor className="w-4 h-4" />,
onSelect: () => setTheme("system"),
keywords: ["system", "システム", "自動"],
},
],
},
]
これで、Command Palette からテーマを素早く切り替えられます。
カスタムアクションの実装例
API 呼び出しやモーダル表示など、より複雑なアクションも実装できます。
typescriptimport { useState } from "react"
import { LogOut, Download, Trash2 } from "lucide-react"
export default function HomePage() {
const [isLoading, setIsLoading] = useState(false)
const commandGroups = [
// 既存のグループ...
{
heading: "アカウント",
items: [
{
id: "logout",
label: "ログアウト",
icon: <LogOut className="w-4 h-4" />,
onSelect: async () => {
if (confirm("ログアウトしますか?")) {
setIsLoading(true)
try {
await fetch("/api/auth/logout", { method: "POST" })
router.push("/login")
} catch (error) {
console.error("ログアウトに失敗しました", error)
} finally {
setIsLoading(false)
}
}
},
keywords: ["logout", "ログアウト", "サインアウト"],
},
{
id: "export-data",
label: "データをエクスポート",
icon: <Download className="w-4 h-4" />,
onSelect: async () => {
setIsLoading(true)
try {
const response = await fetch("/api/export")
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = "data.json"
a.click()
} catch (error) {
console.error("エクスポートに失敗しました", error)
} finally {
setIsLoading(false)
}
},
keywords: ["export", "エクスポート", "ダウンロード", "保存"],
},
{
id: "delete-account",
label: "アカウント削除",
icon: <Trash2 className="w-4 h-4" />,
onSelect: () => {
if (confirm("本当にアカウントを削除しますか?この操作は取り消せません。")) {
router.push("/account/delete")
}
},
keywords: ["delete", "削除", "退会"],
},
],
},
]
このように、Command Palette は単なるナビゲーションだけでなく、様々なアクションのトリガーとして活用できます。
パフォーマンス最適化のポイント
Command Palette を実装する際のパフォーマンス最適化について、いくつかのポイントを紹介します。
useMemo によるコマンドグループの最適化
コマンドグループの定義を useMemo
でメモ化することで、不要な再計算を防ぎます。
typescriptconst commandGroups = useMemo(
() => [
{
heading: 'ナビゲーション',
items: [
// ... コマンドアイテム
],
},
// ... 他のグループ
],
[router, setTheme]
); // 依存する値を明示
useCallback によるハンドラーの最適化
イベントハンドラーも useCallback
でメモ化します。
typescriptconst handleCommandSelect = useCallback(
(item: CommandItemType) => {
item.onSelect();
addToHistory({ id: item.id, label: item.label });
setOpen(false);
},
[addToHistory]
);
大量のコマンドを扱う場合
コマンドが 100 個以上になる場合は、仮想スクロールの導入を検討しましょう。shadcn/ui の Command コンポーネントは、内部で cmdk を使用しており、ある程度の最適化は自動で行われますが、非常に大量のコマンドを扱う場合は追加の対策が必要です。
まとめ
本記事では、shadcn/ui を使った Command Palette の実装方法を詳しく解説しました。
実装した機能のおさらい
# | 機能 | メリット |
---|---|---|
1 | 基本的な Command Palette | モーダル表示とコマンド検索の基盤 |
2 | キーボードショートカット | Cmd+K / Ctrl+K で即座にアクセス |
3 | 個別コマンドショートカット | よく使う機能に直接アクセス |
4 | コマンド履歴 | 過去のコマンドをすぐに再実行 |
5 | グループ化 | カテゴリー別の整理で見つけやすく |
6 | カスタムアクション | ナビゲーション以外にも活用可能 |
shadcn/ui を使うメリットの再確認
shadcn/ui を使うことで、以下のメリットが得られました。
- アクセシビリティ: Radix UI ベースで標準対応
- カスタマイズ性: コンポーネントがプロジェクト内にあるため自由に編集可能
- 軽量: 必要なコンポーネントだけを追加
- TypeScript サポート: 型安全な開発が可能
- デザインの一貫性: Tailwind CSS による統一感
次のステップ
本記事で実装した Command Palette をさらに拡張する場合、以下の機能追加を検討してみてください。
検索アルゴリズムの改善
cmdk のデフォルト検索は優れていますが、ファジー検索や重み付けをカスタマイズすることで、より精度の高い検索が可能になります。
コマンドのネスト
階層構造のあるコマンド(例:設定 > 一般 > 言語)を実装することで、より多くのコマンドを整理して提供できます。
最近使ったファイル
VS Code のように、最近開いたファイルやページをコマンドとして表示する機能も便利です。
AI によるコマンド提案
ユーザーの行動パターンを学習し、次に実行しそうなコマンドを提案する機能も面白いでしょう。
最後に
Command Palette は、ユーザー体験を大きく向上させる UI パターンです。特にパワーユーザーにとっては、マウス操作よりもはるかに効率的にアプリケーションを操作できるため、導入する価値は非常に高いと言えます。
shadcn/ui を使えば、複雑な実装をせずに、高品質な Command Palette を短時間で構築できますので、ぜひあなたのプロジェクトにも取り入れてみてください。
きっと、ユーザーから「このアプリ、使いやすいね!」という声が聞こえてくるはずです。
関連リンク
- article
shadcn/ui で Command Palette を実装:検索・履歴・キーボードショートカット対応
- article
shadcn/ui の asChild/Slot を極める:継承可能な API 設計と責務分離
- article
shadcn/ui カラートークン早見表:ブランドカラー最適化&明暗コントラスト基準
- article
shadcn/ui を Monorepo(Turborepo/pnpm)に導入するベストプラクティス
- article
shadcn/ui と Headless UI/Vanilla Radix を徹底比較:実装量・a11y・可読性の差
- article
shadcn/ui の思想を徹底解剖:なぜ「コピーして使う」アプローチが拡張性に強いのか
- article
shadcn/ui で Command Palette を実装:検索・履歴・キーボードショートカット対応
- article
GPT-5 本番運用の SLO 設計:品質(正確性/再現性)・遅延・コストの三点均衡を保つ
- article
Emotion の「変種(variants)」設計パターン:props→ スタイルの型安全マッピング
- article
Remix でブログをゼロから構築:Markdown・検索・タグ・OGP まで実装
- article
Preact でミニブログを 1 日で公開:ルーティング・MDX・SEO まで一気通貫
- article
Electron スクリーンレコーダー/キャプチャツールを desktopCapturer で作る
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来