In this article, we will explore how to implement a fuzzy search interface with keyboard shortcuts in your project using shadcn/ui components such as Command and Kbd. This guide demonstrates the integration with a Next.js project, but the same approach can be adapted to other frameworks or tech stacks. Additionally, we will discuss the Shadcn KBD UI, which enhances the overall functionality.
The goal is to create a seamless, keyboard-driven interface that enhances user experience by allowing quick navigation and search functionality. By leveraging the power of shadcn/ui and the Shadcn KBD UI, we can build a modern and efficient command palette that is both functional and visually appealing.
What is Shadcn KBD Component and Why Use it?

The Kbd (Keyboard) component in shadcn/ui is a small UI element used to display keyboard keys and shortcuts in a visually clear, consistent way.
Instead of writing plain text like:
- Press Ctrl + K
You present it as:
- [Ctrl] + [K]
It mimics real keyboard keys, making shortcuts easier to scan and understand.
What does Shadcn KBD actually do?
-
Wraps text (like
Ctrl,Cmd,Enter) in a styled key-like box -
Helps group multiple keys into shortcut combinations
-
Improves readability + UX clarity
Why Use Shadcn KBD?
🚀 Makes Shortcuts Discoverable:
Users often don’t know shortcuts exist.
With Kbd:
-
You show, not tell
-
Example: Search → ⌘ + K
Improves UX Clarity
Plain text gets ignored. Visual keys stand out.
-
❌ “Press Ctrl + S”
-
✅ [Ctrl] + [S]
Faster recognition, less cognitive load
Builds Power-User Experience
Apps like:
-
Notion
-
Linear
-
VS Code
…all highlight shortcuts visually.
Kbd helps you achieve that same pro-level polish.
Perfect for Key UI Moments:
Use it in:
-
Buttons → Save [⌘ + S]
-
Tooltips → New Note [⌘ + N]
-
Search bars → [Ctrl + K]
-
Onboarding flows → “Try [Ctrl + K] to search”
Adds UI Polish (Tiny Detail, Big Impact):
It’s a small component, but:
-
Makes your UI feel intentional
-
Improves perceived quality
-
Signals attention to detail
When To Use Shadcn KBD?
Use Kbd if your app has:
-
Keyboard shortcuts
-
Command palette
-
Productivity workflows
-
Developer tools/dashboards
-
Power-user features
Example:
<Kbd>Ctrl</Kbd> + <Kbd>K</Kbd>
The Shadcn Kbd component is not just UI; it’s UX communication.
It helps you:
-
Teach users faster
-
Reduce friction
-
Make your app feel premium
Why Add Command Search?
Command search functionality takes user experience to the next level by enabling users to quickly find and navigate to specific pages or sections within your site. Instead of manually browsing through menus or links, users can instantly locate what they need, saving time and reducing frustration.
Additionally, integrating keyboard shortcuts for frequently accessed pages further enhances usability. This feature allows users to perform actions or navigate to pages with a simple key combination, making the interface more intuitive and efficient. For content-heavy websites or applications, this functionality is invaluable in improving accessibility and user satisfaction.
The Shadcn KBD feature allows users to utilize keyboard shortcuts effectively, making navigation faster and more intuitive.
Brief of Implementation
To better understand the implementation, we will create a basic blog site with the following pages:
-
Home Page
-
Blog Page
-
About Us Page
-
Contact Us Page
The site will also include a Header and Footer for consistent navigation. To save time, we will use pre-built blocks from shadcn/studio, which provides free and premium blocks, components, and themes.
For the search functionality, we will use the Command, Dialog, and Kbd components from shadcn/ui. These components will allow us to create a modern, keyboard-driven command palette and fuzzy search capabilities.
Basic shadcn/ui Project Setup
If you’re integrating this command palette feature into an existing Next.js project, you can skip this section. However, if you’re starting fresh, follow the official shadcn/ui installation guide to set up your project correctly.
For this implementation, we’re using a basic Next.js project named shadcn-cmdk-search with Neutral as the base color theme. You can customize the color scheme and project name according to your preferences.
Once your Next.js and shadcn/ui setup is complete, ensure you have the following components installed from shadcn/ui:
-
Command- For search and command functionality -
Dialog- For the modal overlay -
Kbd- For displaying keyboard shortcuts -
Button- For interactive elements
You can install these components using the shadcn/ui CLI:
npx shadcn-ui@latest add command dialog kbd button
Add Layout
For the header and footer, you can use free shadcn blocks from Shadcn Studio, such as the Shadcn Navbar and Shadcn Footer. These can be quickly integrated into your project using the provided registry.
After obtaining the code for components/layout/Header.tsx and components/layout/Footer.tsx, import and use them in your main layout file:
app/layout.tsx
import Footer from '@/components/layout/Footer'
import Header from '@/components/layout/Header'
...
const navigationData = [
...
]
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang='en' data-scroll-behavior='smooth' className='h-full'>
<body className={`${geistSans.variable} ${geistMono.variable} flex h-full flex-col antialiased`}>
<Header navigationData={navigationData} />
<div className='mx-auto w-full max-w-350 flex-1 border-x'>{children}</div>
<Footer />
</body>
</html>
)
}
This structure ensures a consistent layout across all pages, with navigation and footer components easily reusable.
Add Pages
Now add your desired pages. Use app/page.tsx for the home page, and create /about-us and /contact-us pages. For the content of these pages, you can utilize free blocks from shadcn/studio, such as the Hero Section, Pricing Component, About Us Page, and Contact Us Page.
This approach allows you to quickly scaffold visually appealing and functional pages, ensuring consistency and saving time in development. Each page can be customized further to match your brand and requirements.
Set up Search Data
They searchData will be defined in assets/data/search.ts to provide the structured data source for the command palette’s search functionality.
search.ts
// React Imports
import type { ForwardRefExoticComponent, RefAttributes } from 'react'
// Third-party Imports
import { CircleIcon, type LucideProps } from 'lucide-react'
type SearchData = {
title: string
data: {
icon: ForwardRefExoticComponent<Omit<LucideProps, 'ref'> & RefAttributes<SVGSVGElement>>
name: string
href: string
openInNewTab?: boolean
}[]
}
export const searchData: SearchData[] = [
{
title: 'Pages',
data: [
{
icon: CircleIcon,
name: 'Home',
href: '/'
},
...
]
},
{
title: 'Sections',
data: [
{
icon: CircleIcon,
name: 'Pricing',
href: '/#pricing'
}
]
}
]
- Type Definition: The
SearchDatatype describes an array of groups. Each group contains:-
title:The group’s display name (e.g., “Pages”, “Sections”). -
data: An array of searchable items, each with:-
icon: The Lucide icon component to display. -
name: The display name of the item. -
href: The navigation destination when selected. -
openInNewTab(optional): Opens the link in a new tab if true.
-
-
This structure allows for organized grouping of search results and flexible navigation options.
Create CommandMenu component
Now we will create the CommandMenu component using the Command and Dialog components from shadcn/ui. The Command component provides the search functionality and fuzzy matching powered by the cmdk library. The Dialog component creates an overlay modal that displays on top of any page.
CommandMenu.tsx
'use client'
// React Imports
import { Fragment, useCallback, useEffect, useState } from 'react'
// Next Imports
import { useRouter } from 'next/navigation'
// Third-party Imports
import { DollarSignIcon, LayoutPanelTopIcon, PanelTopIcon, PhoneIcon, SearchIcon, SparklesIcon } from 'lucide-react'
// Component Imports
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator
} from '@/components/ui/command'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Kbd } from '@/components/ui/kbd'
// Data Imports
import { searchData } from '@/assets/data/search'
const CommandMenu = () => {
// States
const [open, setOpen] = useState(false)
const [search, setSearch] = useState('')
// Hooks
const router = useRouter()
useEffect(() => {
const down = (e: KeyboardEvent) => {
if ((e.key === 'k' && (e.metaKey || e.ctrlKey))) {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return
}
e.preventDefault()
setOpen(open => !open)
}
}
document.addEventListener('keydown', down)
return () => document.removeEventListener('keydown', down)
}, [])
const runCommand = useCallback((command: () => unknown) => {
setOpen(false)
command()
}, [])
return (
<>
<Button variant='outline' onClick={() => setOpen(true)} className='w-50 justify-between max-sm:hidden'>
<div className='flex items-center gap-2'>
<SearchIcon />
<span>Search</span>
</div>
<Kbd>⌘ + K</Kbd>
</Button>
<Button variant='outline' size='icon' onClick={() => setOpen(true)} className='sm:hidden'>
<SearchIcon />
<span className='sr-only'>Search</span>
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogHeader className='sr-only'>
<DialogTitle>Search...</DialogTitle>
<DialogDescription>Search for docs, blocks, components, and more.</DialogDescription>
</DialogHeader>
<DialogContent className='overflow-hidden p-0'>
<Command
className='[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5'
filter={(value, search) => {
search = search.toLowerCase()
value = value.toLowerCase()
// Exact match with item name (highest priority)
if (value === search) return 2
// Partial match with item name (medium priority)
if (value.includes(search)) return 1.5
return 0
}}
>
<CommandInput placeholder='Type a command or search...' value={search} onValueChange={setSearch} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{search ? (
searchData.map((searchGroup, index) => (
<Fragment key={index}>
<CommandGroup heading={searchGroup.title}>
{searchGroup.data.map((item, i) => (
<CommandItem
key={i}
onSelect={() =>
runCommand(() => {
if (item.openInNewTab) {
window.open(item.href, '_blank', 'noopener,noreferrer')
} else {
router.push(item.href)
}
})
}
>
<item.icon />
<span>{item.name}</span>
</CommandItem>
))}
</CommandGroup>
{index !== searchData.length - 1 && <CommandSeparator />}
</Fragment>
))
) : (
<CommandGroup heading='Suggestions'>
<CommandItem onSelect={() => runCommand(() => router.push('/'))}>
<SparklesIcon />
<span>Home</span>
</CommandItem>
...
</CommandGroup>
)}
</CommandList>
</Command>
</DialogContent>
</Dialog>
</>
)
}
export default CommandMenu
How it works:
-
State Management: The
CommandMenucomponent uses React state to control the visibility of the command palette (open) and to track the current search input (search). -
Keyboard Listener: A
useEffecthook adds a global keyboard event listener for⌘ + K(on Mac) orCtrl + K(on Windows/Linux). This allows users to open or close the command palette from anywhere in the app, unless they’re focused on an input or editable element. -
Search Data:
-
Imports
searchDatafor dynamic search groups. -
Each item has a name, icon, link, and an option to open in a new tab.
-
-
Filtering Logic: The
filterprop on the<Command>component defines how items are matched:- Converts both the item value and the search input to lowercase.
- Returns 2 for an exact match.
- Returns 1.5 for a partial match (if the item includes the search).
- Returns 0 otherwise.This ranking determines which items appear and in what order.
-
Suggestions: When no search is entered, shows default navigation options.
Add CommandMenu in Header
Now, integrate the CommandMenu component into your Header.tsx so users can easily access the search functionality from any page.
Header.tsx
'use client'
// Next.js Imports
import Link from 'next/link'
import { usePathname } from 'next/navigation'
// Third-party Imports
import { MenuIcon } from 'lucide-react'
// Component Imports
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import CommandMenu from './CommandMenu'
// Utils Imports
import { cn } from '@/lib/utils'
type NavigationItem = {
title: string
href: string
}[]
const Header = ({ navigationData }: { navigationData: NavigationItem }) => {
const pathname = usePathname()
return (
<header className='bg-background sticky top-0 z-50 border-b'>
<div className='mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-7 sm:px-6'>
<div className='text-muted-foreground flex flex-1 items-center gap-8 font-medium md:justify-center lg:gap-16'>
<Link href='/' className={cn('hover:text-primary max-md:hidden', pathname === '/' && 'text-primary')}>
Home
</Link>
...
</div>
<div className='flex items-center gap-2'>
<CommandMenu /> {/* Here we added the CommandMenu */}
...
</div>
</div>
</header>
)
}
export default Header
This ensures the command palette is always accessible from the site header, providing users with a consistent and efficient search experience.
Add shortcut keys functionality
To further enhance navigation, you can assign keyboard shortcuts to frequently accessed pages. This allows users to jump directly to a page using a key combination, such as ⌘ + B for the Blog page.
To implement this, update your searchData structure to include shortcutText (for display) and shortcutKey (for logic):
search.ts
// React Imports
import type { ForwardRefExoticComponent, RefAttributes } from 'react'
// Third-party Imports
import { CircleIcon, type LucideProps } from 'lucide-react'
type SearchData = {
title: string
data: {
icon: ForwardRefExoticComponent<Omit<LucideProps, 'ref'> & RefAttributes<SVGSVGElement>>
name: string
href: string
shortcutText?: string
shortcutKey?: string
openInNewTab?: boolean
}[]
}
export const searchData: SearchData[] = [
{
title: 'Pages',
data: [
{
icon: CircleIcon,
name: 'Home',
shortcutText: '⌘ + O',
shortcutKey: 'o',
href: '/#hero'
},
{
icon: CircleIcon,
name: 'Blog',
shortcutText: '⌘ + B',
shortcutKey: 'b',
href: '/blog'
},
...
]
},
...
]
Then, in your CommandMenu component, listen for these shortcuts globally:
CommandMenu.tsx
// Inside CommandMenu.tsx useEffect
useEffect(() => {
const down = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && !open) {
searchData.forEach(group => {
group.data.forEach(item => {
if (item.shortcutKey && e.key.toLowerCase() === item.shortcutKey) {
e.preventDefault()
if (item.openInNewTab) {
window.open(item.href, '_blank', 'noopener,noreferrer')
} else {
router.push(item.href)
}
}
})
})
}
}
document.addEventListener('keydown', down)
return () => document.removeEventListener('keydown', down)
}, [open, router])
And display the shortcut in the command palette UI:
{item.shortcutText && (
<CommandShortcut>
<Kbd className='bg-background border'>{item.shortcutText}</Kbd>
</CommandShortcut>
)}
This setup allows users to quickly access important pages using custom keyboard shortcuts, improving efficiency and user experience.
Add tags/keywords functionality
To make your command palette even more intuitive, you can add tags or keywords to each search item. This allows users to find pages using related or industry-specific terms, improving the fuzzy search experience.
Update your searchData structure to include an optional tags array for each item:
search.ts
type SearchData = {
title: string
data: {
icon: ForwardRefExoticComponent<Omit<LucideProps, 'ref'> & RefAttributes<SVGSVGElement>>
name: string
href: string
shortcutText?: string
shortcutKey?: string
openInNewTab?: boolean
tags?: string[]
}[]
}
export const searchData: SearchData[] = [
{
title: 'Pages',
data: [
...
{
icon: CircleIcon,
name: 'Contact Us',
shortcutText: '⌘ + I',
shortcutKey: 'i',
href: '/contact-us',
tags: ['contact', 'get in touch', 'support']
}
]
},
...
]
Now, update your CommandMenu component to enhance the filter logic so it also matches against tags:
CommandMenu.tsx
// Inside CommandMenu.tsx
<Command
filter={(value, search, keywords) => {
search = search.toLowerCase()
value = value.toLowerCase()
if (value === search) return 2
if (value.includes(search)) return 1.5
// Match in tags/keywords
if (keywords && keywords.length > 0) {
if (keywords.some(keyword => keyword.toLowerCase() === search)) return 1.25
const extendedValue = value + ' ' + keywords.join(' ').toLowerCase()
if (extendedValue.includes(search)) return 1
}
return 0
}}
>
{/* ... */}
</Command>
And pass the tags as keywords to each CommandItem:
<CommandItem
keywords={item.tags}
// ...
>
{/* ... */}
</CommandItem>
With this setup, users can search for pages using both their names and any relevant tags or keywords you define, making the command palette more flexible and user-friendly.
Set up a search for dynamic pages
In Next.js, you may want to provide search functionality for dynamic pages, such as blog posts or user profiles.
Add dynamic pages
First, create your dynamic pages. For example, you might have blog posts at blog/[blog]/page.tsx and a data file like assets/data/blogData.tsx containing metadata for each blog post.
Add dynamic pages to the search data
To include these dynamic pages in your command palette, map over your dynamic data and inject it into the searchData array:
search.ts
import blogData from './blogData'
type SearchData = {
title: string
data: {
icon: ForwardRefExoticComponent<Omit<LucideProps, 'ref'> & RefAttributes<SVGSVGElement>>
name: string
href: string
shortcutText?: string
shortcutKey?: string
openInNewTab?: boolean
tags?: string[]
}[]
}
export const searchData: SearchData[] = [
...
{
title: 'Blog Posts',
data: [
...blogData.map(category => ({
icon: CircleIcon,
name: category.title,
href: `/blog/${category.slug}`
}))
]
}
]
This approach ensures your command palette always includes up-to-date links to all dynamic content, making navigation seamless for users.
Resources
-
Shadcn Blocks by Shadcn Studio.
Summary
By following this guide, you have implemented a modern, keyboard-driven command palette with fuzzy search, command groups, and keyboard shortcuts using shadcn/ui and Next.js.
This feature not only streamlines navigation and improves accessibility but also delivers a polished, developer-friendly experience that users expect from contemporary web applications.
The approach is flexible and can be extended to support dynamic content, custom tags, and advanced filtering as your project grows.