Tại sao ứng dụng của bạn cần một Command Palette?
Người dùng chuyên nghiệp thường ghét việc phải rời tay khỏi bàn phím để rê chuột. Nếu bạn từng dùng VS Code, Slack hay Linear, bạn sẽ thấy tổ hợp phím Ctrl + K (hoặc Cmd + K) đã trở thành một tiêu chuẩn vàng. Đây là lối tắt giúp người dùng tìm kiếm tài liệu, chuyển trang hoặc đổi giao diện chỉ trong tích tắc.
Command Palette nâng tầm UX cho các ứng dụng SaaS phức tạp. Nó giúp “power user” thao tác nhanh gấp 2-3 lần so với việc click menu thông thường. Tuy nhiên, việc tự xây dựng một thanh lệnh hỗ trợ tốt phím mũi tên, lọc kết quả mờ (fuzzy search) và đạt chuẩn A11y là một thách thức không nhỏ.
So sánh 3 hướng tiếp cận phổ biến
Dựa trên kinh nghiệm triển khai thực tế, tôi đã đúc kết được ưu và nhược điểm của 3 phương pháp sau:
1. Tự code hoàn toàn (Custom Implementation)
Bạn dùng useState quản lý đóng/mở và useEffect để bắt sự kiện phím.
- Ưu điểm: Kiểm soát tuyệt đối logic, bundle size gần như bằng không.
- Nhược điểm: Rất khó xử lý các tiêu chuẩn Accessibility (WAI-ARIA). Việc quản lý focus khi dùng phím mũi tên thường phát sinh nhiều bug vặt.
2. Thư viện KBAR
KBAR là giải pháp mạnh mẽ cho React, hỗ trợ sẵn các hành động phân cấp (nested actions).
- Ưu điểm: Cài đặt nhanh, có sẵn UI cơ bản.
- Nhược điểm: UI đi kèm đôi khi khó tùy chỉnh theo Design System riêng biệt. Bundle size nặng hơn so với các giải pháp headless.
3. Thư viện CMDK (Sự lựa chọn của Vercel và Linear)
Đây là thư viện Headless UI, chỉ tập trung vào logic xử lý phím và tìm kiếm. Đây cũng là “vũ khí” đằng sau thanh tìm kiếm của Raycast.
- Ưu điểm: Cực kỳ nhẹ (khoảng 5kb min-zipped), hỗ trợ fuzzy search siêu nhanh, tuân thủ chặt chẽ tiêu chuẩn A11y.
- Nhược điểm: Bạn cần tự viết CSS hoàn toàn.
Lời khuyên: Đừng cố “tái phát minh bánh xe” ở phần tương tác bàn phím. Combo CMDK + Tailwind CSS hiện là giải pháp tối ưu nhất cho các dự án Next.js hiện đại.
Triển khai Command Palette với CMDK
Khởi tạo môi trường
Đầu tiên, hãy cài đặt bộ đôi thư viện CMDK và Lucide React để lấy icon:
npm install cmdk lucide-react
Xây dựng Component Command Menu
Dưới đây là cấu trúc file components/CommandMenu.tsx. Tôi đã tối ưu logic để menu chỉ xuất hiện khi cần thiết.
"use client"
import React, { useEffect, useState } from 'react'
import { Command } from 'cmdk'
import { Search, User, Settings, LayoutDashboard, LogOut } from 'lucide-react'
import { useRouter } from 'next/navigation'
export const CommandMenu = () => {
const [open, setOpen] = useState(false)
const router = useRouter()
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)
}, [])
const runCommand = (command: () => void) => {
setOpen(false)
command()
}
return (
<Command.Dialog
open={open}
onOpenChange={setOpen}
label="Global Command Menu"
className="fixed inset-0 z-50 flex items-start justify-center pt-[10vh] bg-black/50 backdrop-blur-sm"
>
<div className="bg-white dark:bg-zinc-900 w-full max-w-2xl rounded-xl border border-zinc-200 dark:border-zinc-800 shadow-2xl overflow-hidden">
<div className="flex items-center border-b border-zinc-200 dark:border-zinc-800 px-3">
<Search className="w-5 h-5 text-zinc-400" />
<Command.Input
placeholder="Tìm kiếm lệnh hoặc trang..."
className="w-full p-4 bg-transparent outline-none text-zinc-800 dark:text-zinc-100 placeholder:text-zinc-400"
/>
</div>
<Command.List className="max-h-[400px] overflow-y-auto p-2">
<Command.Empty className="p-4 text-center text-sm text-zinc-500">Không tìm thấy kết quả.</Command.Empty>
<Command.Group heading="Điều hướng" className="px-2 py-3 text-xs font-medium text-zinc-500 uppercase">
<CommandItem onSelect={() => runCommand(() => router.push('/dashboard'))}>
<LayoutDashboard className="mr-2 h-4 w-4" /> Dashboard
</CommandItem>
<CommandItem onSelect={() => runCommand(() => router.push('/profile'))}>
<User className="mr-2 h-4 w-4" /> Hồ sơ
</CommandItem>
</Command.Group>
<Command.Separator className="h-px bg-zinc-200 dark:bg-zinc-800 my-2" />
<Command.Group heading="Hệ thống" className="px-2 py-3 text-xs font-medium text-zinc-500 uppercase">
<CommandItem onSelect={() => runCommand(() => router.push('/settings'))}>
<Settings className="mr-2 h-4 w-4" /> Cài đặt
</CommandItem>
</Command.Group>
</Command.List>
</div>
</Command.Dialog>
)
}
const CommandItem = ({ children, onSelect }: { children: React.ReactNode, onSelect: () => void }) => (
<Command.Item
onSelect={onSelect}
className="flex items-center px-3 py-2 rounded-lg cursor-pointer text-sm text-zinc-700 dark:text-zinc-300 aria-selected:bg-indigo-600 aria-selected:text-white transition-colors"
>
{children}
</Command.Item>
)
Tích hợp vào Root Layout
Để menu hoạt động trên toàn ứng dụng, hãy khai báo nó trong file layout.tsx.
import { CommandMenu } from '@/components/CommandMenu'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="vi">
<body>
{children}
<CommandMenu />
</body>
</html>
)
}
Kinh nghiệm tối ưu từ thực tế
1. Ngăn chặn xung đột với Form
Một lỗi phổ biến là Command Palette tự bật lên khi người dùng đang gõ phím ‘K’ trong input. CMDK đã xử lý tốt việc này. Nếu bạn tự viết logic, hãy nhớ kiểm tra event.target để tránh làm phiền người dùng khi họ đang điền form.
2. Giới hạn số lượng kết quả hiển thị
Đừng render hàng nghìn item trực tiếp vào DOM. Để duy trì tốc độ phản hồi 60fps, bạn chỉ nên hiển thị tối đa 10-15 kết quả phù hợp nhất. Với dữ liệu lớn, hãy thực hiện lọc ở phía server hoặc sử dụng useMemo để tối ưu hiệu năng client.
3. Thêm hiệu ứng chuyển động
Sử dụng Framer Motion để thêm hiệu ứng scale hoặc fade-in nhẹ. Những chi tiết nhỏ này giúp ứng dụng cảm giác mượt mà và cao cấp hơn đáng kể.
Kết luận
Command Palette không chỉ là một thanh tìm kiếm. Đó là trung tâm điều khiển giúp người dùng làm việc hiệu quả hơn. Với Next.js và CMDK, bạn có thể xây dựng tính năng này chỉ trong vài giờ nhưng giá trị UX mang lại là cực lớn. Hãy thử áp dụng ngay cho dự án của mình!

