kbar: The Complete Guide to React Command Palettes
Everything you need to install, configure, and extend kbar β
the library that gives your React app a lightning-fast βK command menu
users will actually reach for.
What is kbar β and why should you care
kbar
is a fast, portable, and extensible
React command palette library
that lets users trigger any action in your application without reaching for the mouse.
You define a list of actions β navigation, theme toggling, search, API calls, whatever you like β
and kbar exposes them through a polished,
keyboard-driven command menu opened with βK on macOS or
Ctrl+K on Windows and Linux.
Think Spotlight, Linear, Vercel, or Raycast β the same experience, portable to any React app in under 30 minutes.
Power users have been quietly demanding this pattern for years.
A
Nielsen Norman Group study on keyboard accessibility
found that experienced users prefer keyboard navigation by a significant margin because it
collapses dozens of clicks into a single intent. kbar translates that insight directly
into a React component: instead of hunting through nested menus or five-tab settings pages,
the user types two words and fires the action. The result is an interface that feels
fast by design, not just fast by hardware.
The library is deliberately headless-first. kbar provides the logic, state machine, and
accessibility wiring β you control every pixel of the UI through composable
sub-components and hooks. It ships at roughly 7 kB gzipped, has zero required peer
dependencies beyond React itself, and works with any styling system: CSS Modules,
Tailwind, styled-components, vanilla CSS, or that proprietary design-system your
design team guards with their lives.
Quick context: kbar was created by Tim Chang and open-sourced in 2021.
It currently has over 5,000 GitHub stars and is actively maintained.
The kbar name itself is a nod to the universal keyboard shortcut that opens it.
Installation and project setup
Getting kbar installed takes about 30 seconds. Open your terminal,
navigate to your React project root, and run one of the following commands depending on
your package manager of choice:
terminal
# npm
npm install kbar
# yarn
yarn add kbar
# pnpm
pnpm add kbar
That’s the entirety of the kbar setup at the dependency level.
No additional polyfills, no CSS imports, no peer dependency gymnastics.
The library targets React 16.8+ (hooks are a hard requirement) and works equally well
with Create React App, Next.js, Remix, Vite, and any other modern React scaffolding.
If you’re on React 18, concurrent mode is fully supported and kbar will take advantage
of useTransition patterns under the hood without any configuration on your part.
Once installed, five named exports from the kbar package cover 90 % of use cases:
KBarProvider, KBarPortal, KBarAnimator,
KBarSearch, and KBarResults.
A sixth β the useKBar hook β is your escape hatch into the internal state machine
whenever you need to build custom triggers, programmatic actions, or deeply custom result renderers.
We’ll meet all of them in the next section.
TypeScript users: kbar ships its own .d.ts declarations β
no need to install a separate @types/kbar package.
Every exported component and hook is fully typed out of the box.
Core concepts: providers, actions, and hooks
Before writing a single line of JSX it’s worth spending two minutes on kbar’s mental model,
because it will save you from re-reading the docs three times later.
kbar is built around a central state machine exposed through React context.
The KBarProvider sits at the top of your component tree, owns the state,
and passes it down. Every other kbar component β search input, results list, animator β
consumes that context. This means you can render the palette’s UI anywhere
in the tree and it will always be in sync with the shared state.
The Action is kbar’s atomic unit. Each action is a plain JavaScript object
with a handful of properties. The id is the unique identifier.
The name is what users see and search against.
The perform callback is what executes when the user selects the action.
Everything else β section, icon, shortcut,
keywords, parent β is optional but powerful.
| Property | Type | Required | Description |
|---|---|---|---|
id |
string |
β | Unique identifier used for nesting and deduplication |
name |
string |
β | Display label and primary search target |
perform |
() => void |
β οΈ * | Callback executed on selection. Omit for parent-only grouping actions |
section |
string |
β | Visual group label in the results list |
icon |
ReactNode |
β | Icon rendered next to the action name |
shortcut |
string[] |
β | Key sequences shown as hints, e.g. ['g', 'd'] |
keywords |
string |
β | Extra search terms beyond the name |
parent |
string |
β | ID of a parent action for nested drill-downs |
subtitle |
string |
β | Secondary label rendered beneath the action name |
The useKBar hook is your programmatic interface to the same state machine.
It accepts a selector function β similar to how Redux’s useSelector works β
and returns a { state, query } tuple.
state gives you the current search string, active action, and visibility flag.
query exposes imperative methods: query.toggle() opens or closes
the palette, query.setCurrentRootAction(id) teleports the view to a nested
action group, and query.registerActions(actions) lets you add actions
dynamically at runtime β which becomes essential once your app grows beyond a handful
of static commands.
Building your first command palette
Enough theory. Here’s the complete kbar getting started example β
a fully functional React command palette from zero.
We’ll start with the entry point of the app and work our way down to the styled result item.
Step 1 β Wrap your app in KBarProvider
src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { KBarProvider } from 'kbar'
import App from './App'
import { actions } from './actions'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<KBarProvider actions={actions}>
<App />
</KBarProvider>
</React.StrictMode>
)
Step 2 β Define your actions
src/actions.ts
import { Action } from 'kbar'
export const actions: Action[] = [
// ββ Navigation ββββββββββββββββββββββββββββββ
{
id: 'go-home',
name: 'Go to Home',
section: 'Navigation',
shortcut: ['g', 'h'],
keywords: 'home dashboard start',
icon: 'π ',
perform: () => (window.location.pathname = '/'),
},
{
id: 'go-settings',
name: 'Open Settings',
section: 'Navigation',
shortcut: ['g', 's'],
keywords: 'preferences config account',
icon: 'βοΈ',
perform: () => (window.location.pathname = '/settings'),
},
// ββ Theme ββββββββββββββββββββββββββββββββββββ
{
id: 'theme',
name: 'Change Theme',
section: 'Preferences',
icon: 'π¨',
// No `perform` β this is a parent grouping action
},
{
id: 'theme-dark',
name: 'Dark Mode',
parent: 'theme',
icon: 'π',
perform: () => document.documentElement.setAttribute('data-theme', 'dark'),
},
{
id: 'theme-light',
name: 'Light Mode',
parent: 'theme',
icon: 'βοΈ',
perform: () => document.documentElement.setAttribute('data-theme', 'light'),
},
]
Step 3 β Render the palette UI
Now create a CommandPalette component that assembles the visual shell.
This is where you own the look and feel entirely.
KBarPortal renders into a React portal (outside the normal DOM tree,
avoiding any z-index or overflow: hidden nightmares),
while KBarAnimator handles the entrance and exit transitions via
a simple style prop.
src/CommandPalette.tsx
import {
KBarPortal,
KBarAnimator,
KBarSearch,
KBarResults,
useMatches,
ActionImpl,
ActionId,
} from 'kbar'
export function CommandPalette() {
return (
<KBarPortal>
{/* Overlay backdrop */}
<div style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.6)',
backdropFilter: 'blur(4px)',
zIndex: 9998,
}} />
<KBarAnimator
style={{
position: 'fixed',
top: '20%',
left: '50%',
transform: 'translateX(-50%)',
width: 'min(520px, 90vw)',
background: '#1e2030',
borderRadius: '12px',
border: '1px solid #2e3250',
boxShadow: '0 24px 64px rgba(0,0,0,0.5)',
overflow: 'hidden',
zIndex: 9999,
}}
>
<KBarSearch
style={{
width: '100%',
padding: '16px',
fontSize: '16px',
border: 'none',
outline: 'none',
background: 'transparent',
color: '#e2e8f0',
borderBottom: '1px solid #2e3250',
}}
defaultPlaceholder="Type a command or searchβ¦"
/>
<RenderResults />
</KBarAnimator>
</KBarPortal>
)
}
// ββ Results renderer ββββββββββββββββββββββββββ
function RenderResults() {
const { results, rootActionId } = useMatches()
return (
<KBarResults
items={results}
onRender={({ item, active }) =>
typeof item === 'string'
? <SectionHeader label={item} />
: <ResultItem action={item} active={active} currentRootActionId={rootActionId} />
}
/>
)
}
// ββ Section header βββββββββββββββββββββββββββββ
const SectionHeader = ({ label }: { label: string }) => (
<div style={{
padding: '6px 12px',
fontSize: '11px',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.07em',
color: '#585b70',
}}>
{label}
</div>
)
// ββ Individual result item ββββββββββββββββββββββ
const ResultItem = React.forwardRef(
(
{ action, active, currentRootActionId }:
{ action: ActionImpl; active: boolean; currentRootActionId: ActionId },
ref: React.Ref<HTMLDivElement>
) => {
const ancestors = React.useMemo(() => {
if (!currentRootActionId) return action.ancestors
const idx = action.ancestors.findIndex(a => a.id === currentRootActionId)
return action.ancestors.slice(idx + 1)
}, [action, currentRootActionId])
return (
<div
ref={ref}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 12px',
background: active ? 'rgba(108,99,255,0.2)' : 'transparent',
cursor: 'pointer',
borderRadius: '6px',
margin: '0 6px',
}}
>
{action.icon && <span>{action.icon}</span>}
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{ancestors.map(ancestor => (
<React.Fragment key={ancestor.id}>
<span style={{ color: '#585b70', fontSize: '13px' }}>
{ancestor.name}
</span>
<span style={{ color: '#585b70' }}>βΊ</span>
</React.Fragment>
))}
<span style={{ color: '#e2e8f0', fontSize: '14px' }}>
{action.name}
</span>
</div>
{action.subtitle && (
<span style={{ fontSize: '12px', color: '#585b70' }}>
{action.subtitle}
</span>
)}
</div>
{action.shortcut?.length > 0 && (
<div style={{ display: 'flex', gap: '4px' }}>
{action.shortcut.map(sc => (
<kbd
key={sc}
style={{
background: '#22263a',
border: '1px solid #2e3250',
borderRadius: '4px',
padding: '2px 6px',
fontSize: '11px',
fontFamily: 'monospace',
color: '#8892aa',
}}
>
{sc}
</kbd>
))}
</div>
)}
</div>
)
}
)
ResultItem.displayName = 'ResultItem'
Step 4 β Add the palette to your App
src/App.tsx
import { CommandPalette } from './CommandPalette'
export default function App() {
return (
<>
<CommandPalette />
{/* your app content */}
<main>
<h1>Press <kbd>βK</kbd> to open the command palette</h1>
</main>
</>
)
}
At this point you have a fully working
React command palette with keyboard navigation,
fuzzy search, section grouping, shortcut hints, and nested actions.
kbar automatically registers the βK / Ctrl+K global listener
inside KBarProvider, so there’s nothing else to wire up for the default trigger.
Press the shortcut, type anything from your actions list, use arrow keys to navigate,
hit Enter to execute, and Escape to close. It just works.
Next.js users: Place <CommandPalette />
inside your root layout.tsx so the palette is available
on every page without re-mounting on navigation.
Advanced usage: nesting, sections, dynamic actions
The kbar advanced usage that separates good implementations from great ones
comes down to three patterns: action nesting, dynamic action registration,
and programmatic control via useKBar.
Each one unlocks a different dimension of user experience β nesting gives you
multi-step flows, dynamic registration gives you live data, and programmatic control
gives you deep integration with the rest of your app’s state.
Nested actions and drill-down flows
When an action has no perform callback but has children (other actions whose
parent property matches its id), kbar treats it as a navigable
group. Selecting it doesn’t execute anything β it transitions the palette into a
sub-view showing only that group’s children.
A breadcrumb at the top of the search input shows the current depth, and pressing
Backspace on an empty input navigates back up. This is how tools like
Linear implement their “Change assignee β [list of team members]” flows.
src/actions/statusActions.ts
// Parent action β no perform, just a container
{
id: 'change-status',
name: 'Change Issue Status',
section: 'Issues',
icon: 'π',
}
// Child actions β each references the parent
{
id: 'status-todo',
name: 'To Do',
parent: 'change-status',
perform: () => updateStatus('todo'),
}
{
id: 'status-in-progress',
name: 'In Progress',
parent: 'change-status',
perform: () => updateStatus('in-progress'),
}
{
id: 'status-done',
name: 'Done',
parent: 'change-status',
perform: () => updateStatus('done'),
}
Dynamic actions with useRegisterActions
Static actions defined at startup are fine for navigation and settings, but the real
power emerges when you register actions dynamically based on app state.
The useRegisterActions hook accepts an array of actions and an optional
dependency array β whenever the dependencies change, kbar unregisters the old actions
and registers the new ones. This lets you expose context-sensitive commands:
different actions based on which page the user is on, which items are selected,
or which permissions they hold.
src/hooks/useDocumentActions.ts
import { useRegisterActions } from 'kbar'
export function useDocumentActions(documents: Doc[]) {
useRegisterActions(
documents.map(doc => ({
id: `doc-${doc.id}`,
name: doc.title,
subtitle: `Last edited ${formatDate(doc.updatedAt)}`,
section: 'Recent Documents',
icon: 'π',
keywords: doc.tags.join(' '),
perform: () => router.push(`/docs/${doc.slug}`),
})),
[documents]
)
}
Programmatic control with useKBar
Sometimes you want to open the palette from a button click, a menu item, or a custom
keyboard shortcut that doesn’t fit the default βK trigger.
useKBar gives you the query object, which exposes
query.toggle() for exactly this purpose.
You can also call query.setCurrentRootAction(id) to open the palette
directly inside a specific nested action group β perfect for a “change theme”
button that should open the palette pre-navigated to the theme sub-menu.
src/components/Toolbar.tsx
import { useKBar } from 'kbar'
export function Toolbar() {
const { query } = useKBar()
return (
<header>
<button
onClick={() => query.toggle()}
aria-label="Open command palette"
aria-keyshortcuts="Meta+k Control+k"
>
<SearchIcon />
<span>Searchβ¦</span>
<kbd>βK</kbd>
</button>
<button
onClick={() => {
query.setCurrentRootAction('theme')
query.toggle()
}}
>
π¨ Theme
</button>
</header>
)
}
Keyboard shortcuts and custom triggers
React keyboard shortcuts inside kbar operate on two levels.
The first level is the global shortcut that opens the palette itself β
configurable through the options prop on KBarProvider.
The second level is per-action shortcuts: the shortcut array on each action
defines a key sequence that triggers that action without the palette being open.
kbar listens for these sequences globally and fires the action’s perform
callback as soon as the sequence is completed, keeping the palette closed.
Customizing the global open trigger is straightforward.
The options prop accepts a disableDefaultShortcuts boolean
that removes the βK listener, letting you register your own via
useEffect and call query.toggle() on your chosen key combination.
This is useful when your app already uses βK for something else,
or when you’re building a product that ships in an electron shell with its own
global shortcut layer.
src/main.tsx β custom trigger example
<KBarProvider
actions={actions}
options={{
disableDefaultShortcuts: true, // removes βK listener
enableHistory: true, // enables back/forward in palette history
callbacks: {
onOpen: () => console.log('palette opened'),
onClose: () => console.log('palette closed'),
onSelectAction: (action) => analytics.track('command_used', { id: action.id }),
}
}}
>
The callbacks option inside options is underrated and often
overlooked in tutorials. onSelectAction fires every time a user executes
a command β hook into it for analytics, audit logs, or feature usage tracking.
You’ll quickly discover which actions your power users rely on daily and which
ones you can safely remove. Consider it free product telemetry with three lines of code.
Accessibility and performance
kbar takes accessibility seriously at the library level, which is refreshing.
The search input renders as a proper <input type="text"> with a
role="combobox" and the appropriate aria-expanded,
aria-haspopup, and aria-controls attributes.
The results list receives role="listbox", each item gets
role="option", and the active item is tracked with aria-activedescendant.
Screen readers can follow the interaction without custom ARIA plumbing on your side.
Performance-wise, the fuzzy search algorithm kbar uses is synchronous and fast enough for
lists of a few hundred actions without any noticeable lag. If your actions list grows into
the thousands β think a documentation search across every page of a large site β you’ll
want to move the search computation into a Web Worker or replace kbar’s default matcher
with a library like Fuse.js
or fuzzysearch.
kbar doesn’t expose a direct hook to override the search algorithm at the moment,
but you can work around this by pre-filtering your action list with a custom hook before
passing it to useRegisterActions.
One performance detail worth noting: KBarPortal uses React’s
createPortal to mount outside your main component tree, which means the
palette overlay won’t cause layout shifts or trigger reflows in the rest of your UI.
It also means your CSS overflow: hidden containers won’t clip it β
a common frustration with modal implementations that don’t use portals.
On the bundle side, kbar is tree-shakable, so if you import only the hooks you need
in a particular file, unused exports won’t end up in your production bundle.
kbar vs cmdk β which one to pick
The two dominant React command palette libraries in 2024 are kbar and
cmdk
(used by shadcn/ui and Vercel). They solve the same problem but from different angles.
kbar is opinionated about the action model β it gives you a full state machine,
global shortcut registration, action nesting, history, and keyboard-driven navigation
out of the box. cmdk is more of a headless primitive: it gives you the
command menu scaffold but delegates nearly everything else to you, including shortcut
management and action structure. Neither is objectively better β the right choice depends
on what you’re building.
- Choose kbar if you want batteries included β built-in global shortcut, action nesting, search-by-keywords, analytics callbacks, and a clear data model for actions.
- Choose cmdk if you’re already using shadcn/ui, need deeper Radix UI integration, or want a more minimal API surface to customize from scratch.
- Neither is a good fit if you need async search against a remote API as the primary UX β in that case, consider building on top of cmdk with a custom fetcher or evaluating Algolia’s DocSearch for documentation use cases.
In practice, many teams start with kbar and add cmdk later only if they’re deep in the
shadcn/ui ecosystem and want visual consistency with the rest of their component library.
If you’re starting fresh, kbar’s more opinionated approach means less boilerplate for
the 80 % case and a well-documented path for the remaining 20 %.
Note on bundle size: kbar is ~7 kB gzipped; cmdk is ~3.5 kB.
For most apps the difference is negligible, but if you’re on a strict
performance budget it’s worth factoring in.
β Frequently Asked Questions
kbar is a React library that adds a fully accessible, keyboard-driven
command palette to any React application. It works by wrapping your app in a
KBarProvider, defining an actions array of commands,
and rendering KBarPortal with its sub-components anywhere in the tree.
Users open it with βK (macOS) or Ctrl+K (Windows/Linux),
type to fuzzy-search through actions, navigate with arrow keys, and execute with Enter.
The library handles all ARIA wiring, keyboard event management, and portal mounting β
you control the visual design.
Run npm install kbar (or yarn add kbar / pnpm add kbar)
in your project root. Import KBarProvider, KBarPortal,
KBarAnimator, KBarSearch, and KBarResults
from 'kbar'. Wrap your root component with KBarProvider
and pass your actions array as the actions prop.
Drop <CommandPalette /> anywhere inside KBarProvider.
The global βK shortcut is registered automatically β no further
configuration required for a working implementation.
Yes. Each action accepts a parent property containing the id
of another action, creating a nested drill-down group. Actions also accept a
section string to visually group commands under a labelled header in the
results list. The keywords string extends searchability beyond the
action’s display name. Selecting a parent action (one with no perform
callback) transitions the palette into the sub-view; pressing Backspace
on an empty input navigates back to the parent level.
Semantic Core
Keyword clusters used for SEO optimization of this article.
- kbar
- kbar React
- kbar command palette
- React command palette library
- kbar tutorial
- kbar getting started
- kbar installation
- kbar setup
- kbar example
- kbar advanced usage
- React βK menu
- React cmd+k interface
- React command menu
- React keyboard shortcuts
- React searchable menu
- React hotkeys
- cmdk alternative
- spotlight-like UI React
- KBarProvider
- KBarPortal
- KBarAnimator
- KBarSearch
- KBarResults
- useKBar hook
- useRegisterActions
- command palette UI
- keyboard-driven navigation
- accessible command interface