DEV Community

Justin Clark
Justin Clark

Posted on

Building Command Palettes with kbar in React

kbar is a fast, portable, and extensible ⌘K interface for React applications. It provides a complete command palette solution with keyboard shortcuts, search functionality, and customizable UI components. kbar makes it easy to create powerful command menus similar to those found in VS Code, Linear, and other modern applications. This guide walks through setting up and creating command palettes using kbar with React, from installation to a working implementation.

Demoanimation

Prerequisites

Before you begin, make sure you have:

  • Node.js version 14.0 or higher installed
  • npm, yarn, or pnpm package manager
  • A React project (version 16.8 or higher) or create-react-app setup
  • Basic knowledge of React hooks (useState, useEffect)
  • Familiarity with JavaScript/TypeScript
  • Understanding of keyboard shortcuts

Installation

Install kbar using your preferred package manager:

npm install kbar
Enter fullscreen mode Exit fullscreen mode

Or with yarn:

yarn add kbar
Enter fullscreen mode Exit fullscreen mode

Or with pnpm:

pnpm add kbar
Enter fullscreen mode Exit fullscreen mode

After installation, your package.json should include:

{
  "dependencies": {
    "kbar": "^0.1.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Project Setup

kbar requires wrapping your application with KBarProvider and including the UI components. Let's set up the basic structure.

First Example / Basic Usage

Let's create a simple command palette. Update your src/App.jsx:

// src/App.jsx
import React from 'react';
import {
  KBarProvider,
  KBarPortal,
  KBarPositioner,
  KBarAnimator,
  KBarSearch,
  KBarResults,
  useMatches
} from 'kbar';
import './App.css';

// Define actions for the command palette
const actions = [
  {
    id: 'blog',
    name: 'Blog',
    shortcut: ['b'],
    keywords: 'writing words articles',
    perform: () => {
      console.log('Navigate to blog');
      window.location.pathname = '/blog';
    }
  },
  {
    id: 'contact',
    name: 'Contact',
    shortcut: ['c'],
    keywords: 'email message',
    perform: () => {
      console.log('Navigate to contact');
      window.location.pathname = '/contact';
    }
  },
  {
    id: 'github',
    name: 'Open GitHub',
    shortcut: ['g', 'h'],
    keywords: 'code repository',
    perform: () => {
      window.open('https://github.com', '_blank');
    }
  }
];

// Component to render search results
function RenderResults() {
  const { results } = useMatches();

  return (
    <KBarResults
      items={results}
      onRender={({ item, active }) =>
        typeof item === 'string' ? (
          <div style={{ padding: '8px 16px', fontSize: '12px', opacity: 0.5 }}>
            {item}
          </div>
        ) : (
          <div
            style={{
              padding: '12px 16px',
              background: active ? '#eee' : 'transparent',
              cursor: 'pointer',
              display: 'flex',
              alignItems: 'center',
              gap: '12px'
            }}
          >
            <div style={{ display: 'flex', flexDirection: 'column' }}>
              <span>{item.name}</span>
              {item.subtitle && (
                <span style={{ fontSize: '12px', opacity: 0.6 }}>
                  {item.subtitle}
                </span>
              )}
            </div>
          </div>
        )
      }
    />
  );
}

// Main app component
function App() {
  return (
    <KBarProvider actions={actions}>
      <KBarPortal>
        <KBarPositioner>
          <KBarAnimator>
            <KBarSearch />
            <RenderResults />
          </KBarAnimator>
        </KBarPositioner>
      </KBarPortal>
      <div className="App">
        <h1>My Application</h1>
        <p>Press ⌘K (or Ctrl+K) to open the command palette</p>
      </div>
    </KBarProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

This creates a basic command palette that opens with ⌘K (or Ctrl+K) and displays searchable actions.

Understanding the Basics

kbar provides command palette components:

  • KBarProvider: Context provider that wraps your app
  • KBarPortal: Portal for rendering the command palette
  • KBarPositioner: Positions the palette on screen
  • KBarAnimator: Animates the palette appearance
  • KBarSearch: Search input field
  • KBarResults: Displays filtered results
  • useMatches: Hook to get filtered results

Key concepts:

  • Actions: Array of command objects with id, name, shortcut, keywords, and perform function
  • Keyboard shortcuts: Press ⌘K (Mac) or Ctrl+K (Windows/Linux) to open
  • Search: Automatically filters actions based on name and keywords
  • Custom rendering: Use onRender prop to customize result appearance

Here's an example with more features:

// src/AdvancedCommandPalette.jsx
import React from 'react';
import {
  KBarProvider,
  KBarPortal,
  KBarPositioner,
  KBarAnimator,
  KBarSearch,
  KBarResults,
  useMatches
} from 'kbar';

const advancedActions = [
  {
    id: 'home',
    name: 'Home',
    shortcut: ['h'],
    keywords: 'home page',
    perform: () => console.log('Navigate to home')
  },
  {
    id: 'settings',
    name: 'Settings',
    shortcut: ['s'],
    keywords: 'preferences config',
    perform: () => console.log('Open settings'),
    icon: '⚙️',
    subtitle: 'Configure your preferences'
  },
  {
    id: 'theme',
    name: 'Toggle Theme',
    shortcut: ['t'],
    keywords: 'dark light mode',
    perform: () => console.log('Toggle theme'),
    icon: '🌓'
  }
];

function AdvancedRenderResults() {
  const { results } = useMatches();

  return (
    <KBarResults
      items={results}
      onRender={({ item, active }) =>
        typeof item === 'string' ? (
          <div style={{
            padding: '8px 16px',
            fontSize: '12px',
            textTransform: 'uppercase',
            color: '#666',
            fontWeight: 600
          }}>
            {item}
          </div>
        ) : (
          <div
            style={{
              padding: '12px 16px',
              background: active ? '#f0f0f0' : 'transparent',
              cursor: 'pointer',
              display: 'flex',
              alignItems: 'center',
              gap: '12px',
              borderLeft: active ? '3px solid #007bff' : '3px solid transparent'
            }}
          >
            {item.icon && <span style={{ fontSize: '20px' }}>{item.icon}</span>}
            <div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
              <span style={{ fontWeight: 500 }}>{item.name}</span>
              {item.subtitle && (
                <span style={{ fontSize: '12px', opacity: 0.6 }}>
                  {item.subtitle}
                </span>
              )}
            </div>
            {item.shortcut && (
              <div style={{ display: 'flex', gap: '4px' }}>
                {item.shortcut.map((key) => (
                  <kbd
                    key={key}
                    style={{
                      padding: '4px 8px',
                      backgroundColor: '#ddd',
                      borderRadius: '4px',
                      fontSize: '11px'
                    }}
                  >
                    {key}
                  </kbd>
                ))}
              </div>
            )}
          </div>
        )
      }
    />
  );
}

function AdvancedCommandPalette() {
  return (
    <KBarProvider actions={advancedActions}>
      <KBarPortal>
        <KBarPositioner>
          <KBarAnimator>
            <KBarSearch placeholder="Type a command or search..." />
            <AdvancedRenderResults />
          </KBarAnimator>
        </KBarPositioner>
      </KBarPortal>
      <div style={{ padding: '40px' }}>
        <h1>Advanced Command Palette</h1>
        <p>Press ⌘K to open</p>
      </div>
    </KBarProvider>
  );
}

export default AdvancedCommandPalette;
Enter fullscreen mode Exit fullscreen mode

Practical Example / Building Something Real

Let's build a comprehensive command palette with grouped actions and navigation:

// src/ComprehensiveCommandPalette.jsx
import React from 'react';
import {
  KBarProvider,
  KBarPortal,
  KBarPositioner,
  KBarAnimator,
  KBarSearch,
  KBarResults,
  useMatches
} from 'kbar';

const comprehensiveActions = [
  {
    id: 'home',
    name: 'Home',
    shortcut: ['h'],
    keywords: 'home page main',
    perform: () => console.log('Navigate to home'),
    icon: '🏠'
  },
  {
    id: 'blog',
    name: 'Blog',
    shortcut: ['b'],
    keywords: 'writing articles posts',
    perform: () => console.log('Navigate to blog'),
    icon: '📝'
  },
  {
    id: 'contact',
    name: 'Contact',
    shortcut: ['c'],
    keywords: 'email message reach',
    perform: () => console.log('Navigate to contact'),
    icon: '📧',
    subtitle: 'Get in touch with us'
  },
  {
    id: 'settings',
    name: 'Settings',
    shortcut: ['s'],
    keywords: 'preferences config',
    perform: () => console.log('Open settings'),
    icon: '⚙️',
    subtitle: 'Configure your preferences'
  },
  {
    id: 'theme',
    name: 'Toggle Theme',
    shortcut: ['t'],
    keywords: 'dark light mode',
    perform: () => console.log('Toggle theme'),
    icon: '🌓'
  },
  {
    id: 'github',
    name: 'Open GitHub',
    shortcut: ['g', 'h'],
    keywords: 'code repository',
    perform: () => window.open('https://github.com', '_blank'),
    icon: '🐙'
  },
  {
    id: 'docs',
    name: 'Documentation',
    shortcut: ['d'],
    keywords: 'docs help guide',
    perform: () => window.open('https://docs.example.com', '_blank'),
    icon: '📚'
  }
];

function ComprehensiveRenderResults() {
  const { results } = useMatches();

  return (
    <KBarResults
      items={results}
      maxHeight={400}
      onRender={({ item, active }) =>
        typeof item === 'string' ? (
          <div style={{
            padding: '8px 20px',
            fontSize: '11px',
            textTransform: 'uppercase',
            color: '#666',
            letterSpacing: '1px',
            fontWeight: 600,
            backgroundColor: '#f8f9fa'
          }}>
            {item}
          </div>
        ) : (
          <div
            style={{
              padding: '12px 20px',
              backgroundColor: active ? '#e7f3ff' : 'transparent',
              borderLeft: active ? '3px solid #007bff' : '3px solid transparent',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'space-between',
              cursor: 'pointer',
              transition: 'all 0.2s ease'
            }}
          >
            <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
              {item.icon && (
                <div style={{ fontSize: '20px', width: '24px', textAlign: 'center' }}>
                  {item.icon}
                </div>
              )}
              <div>
                <div style={{ color: '#333', fontSize: '14px', fontWeight: 500 }}>
                  {item.name}
                </div>
                {item.subtitle && (
                  <div style={{ color: '#666', fontSize: '12px', marginTop: '2px' }}>
                    {item.subtitle}
                  </div>
                )}
              </div>
            </div>
            {item.shortcut && (
              <div style={{ display: 'flex', gap: '4px' }}>
                {item.shortcut.map((key) => (
                  <kbd
                    key={key}
                    style={{
                      padding: '4px 8px',
                      backgroundColor: '#f0f0f0',
                      borderRadius: '4px',
                      fontSize: '11px',
                      color: '#666',
                      fontFamily: 'monospace',
                      border: '1px solid #ddd'
                    }}
                  >
                    {key}
                  </kbd>
                ))}
              </div>
            )}
          </div>
        )
      }
    />
  );
}

function ComprehensiveCommandPalette() {
  return (
    <KBarProvider actions={comprehensiveActions}>
      <KBarPortal>
        <KBarPositioner
          style={{
            backgroundColor: 'rgba(0, 0, 0, 0.5)',
            zIndex: 9999
          }}
        >
          <KBarAnimator
            style={{
              maxWidth: '600px',
              width: '100%',
              backgroundColor: 'white',
              borderRadius: '12px',
              overflow: 'hidden',
              boxShadow: '0 16px 70px rgba(0, 0, 0, 0.3)'
            }}
          >
            <KBarSearch
              style={{
                padding: '16px 20px',
                fontSize: '16px',
                width: '100%',
                boxSizing: 'border-box',
                outline: 'none',
                border: 'none',
                borderBottom: '1px solid #eee'
              }}
              placeholder="Type a command or search..."
            />
            <ComprehensiveRenderResults />
          </KBarAnimator>
        </KBarPositioner>
      </KBarPortal>
      <div style={{ padding: '40px', minHeight: '100vh' }}>
        <h1>Comprehensive Command Palette</h1>
        <p>Press ⌘K (or Ctrl+K) to open the command palette</p>
        <div style={{ marginTop: '20px' }}>
          <h2>Available Commands:</h2>
          <ul>
            <li><strong>h</strong> - Navigate to Home</li>
            <li><strong>b</strong> - Navigate to Blog</li>
            <li><strong>c</strong> - Navigate to Contact</li>
            <li><strong>s</strong> - Open Settings</li>
            <li><strong>t</strong> - Toggle Theme</li>
            <li><strong>g h</strong> - Open GitHub</li>
            <li><strong>d</strong> - Open Documentation</li>
          </ul>
        </div>
      </div>
    </KBarProvider>
  );
}

export default ComprehensiveCommandPalette;
Enter fullscreen mode Exit fullscreen mode

Update your App.jsx:

// src/App.jsx
import React from 'react';
import ComprehensiveCommandPalette from './ComprehensiveCommandPalette';
import './App.css';

function App() {
  return (
    <div className="App">
      <ComprehensiveCommandPalette />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

This example demonstrates:

  • Multiple actions with icons
  • Keyboard shortcuts display
  • Grouped results
  • Custom styling
  • Search functionality
  • Action execution
  • Responsive design

Common Issues / Troubleshooting

  1. Command palette not opening: Make sure you've wrapped your app with KBarProvider. The default shortcut is ⌘K (Mac) or Ctrl+K (Windows/Linux). Check browser console for errors.

  2. Actions not showing: Verify that your actions array is properly formatted with required fields (id, name, perform). Check that KBarResults is inside KBarAnimator.

  3. Search not working: Ensure KBarSearch is included and useMatches hook is used correctly. Actions are filtered by name and keywords automatically.

  4. Styling issues: kbar is unstyled by default. Add custom styles to KBarPositioner, KBarAnimator, and result items. Use inline styles or CSS classes.

  5. Keyboard shortcuts not working: Check that no other components are intercepting keyboard events. Ensure KBarProvider is at the root level of your app.

  6. Portal rendering issues: Make sure KBarPortal is included. The portal renders the command palette outside the normal DOM hierarchy.

Next Steps

Now that you have an understanding of kbar:

  • Explore action groups and nested actions
  • Learn about custom action handlers
  • Implement dynamic actions based on app state
  • Add animations and transitions
  • Create context-aware commands
  • Integrate with routing libraries
  • Check the official repository: https://github.com/timc1/kbar

Summary

You've successfully set up kbar in your React application and created command palettes with keyboard shortcuts, search functionality, and customizable actions. kbar provides a fast, portable solution for building ⌘K interfaces in React applications.

SEO Keywords

kbar
kbar React
kbar command palette
React ⌘K menu
kbar installation
React command menu
kbar tutorial
React command palette library
kbar example
React keyboard shortcuts
kbar setup
React cmd+k interface
kbar getting started
React searchable menu
kbar advanced usage

Top comments (0)