npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

use-kbd

v0.8.0

Published

Omnibars, editable hotkeys, search, and keyboard-navigation for React apps

Readme

use-kbd

npm version

Omnibars, editable hotkeys, search, and keyboard-navigation for React apps.

📖 Documentation & Demos →

Also in production at ctbk.dev and awair.runsascoded.com.

Quick Start

npm install use-kbd  # or: pnpm add use-kbd
import { HotkeysProvider, ShortcutsModal, Omnibar, LookupModal, SequenceModal, useAction } from 'use-kbd'
import 'use-kbd/styles.css'

function App() {
  return (
    <HotkeysProvider>
      <Dashboard />
      <ShortcutsModal />  {/* "?" modal: view/edit key-bindings */}
      <Omnibar />         {/* "⌘K" omnibar: search and select actions */}
      <LookupModal />     {/* "⌘⇧K": look up actions by key-binding */}
      <SequenceModal />   {/* Inline display for key-sequences in progress */}
    </HotkeysProvider>
  )
}

function Dashboard() {
  const { save } = useDocument()  // Function to expose via hotkeys / omnibar

  // Wrap function as "action", with keybinding(s) and omnibar keywords
  useAction('doc:save', {
    label: 'Save document',
    group: 'Document',
    defaultBindings: ['meta+s'],
    handler: save,
  })

  return <Editor />
}

Basic steps

  1. Drop-in UI components:
    • ShortcutsModal: view/edit key-bindings
    • Omnibar: search and select actions
    • LookupModal: look up actions by key-binding
    • SequenceModal: autocomplete multi-key sequences
  2. Register functions as "actions" with useAction
  3. Easy theming with CSS variables

Core Concepts

Actions

Register any function with useAction:

useAction('view:toggle-sidebar', {
  label: 'Toggle sidebar',
  group: 'View',
  defaultBindings: ['meta+b', 'meta+\\'],
  keywords: ['panel', 'navigation'],
  handler: () => setSidebarOpen(prev => !prev),
})

Actions automatically unregister when the component unmounts—no cleanup needed.

Conditionally disable actions with enabled:

useAction('doc:save', {
  label: 'Save',
  defaultBindings: ['meta+s'],
  enabled: hasUnsavedChanges,  // Action hidden when false
  handler: save,
})

Protect essential bindings from removal with protected:

useAction('app:shortcuts', {
  label: 'Show shortcuts',
  defaultBindings: ['?'],
  protected: true,  // Users can add bindings, but can't remove this one
  handler: () => openShortcutsModal(),
})

Sequences

Multi-key sequences like Vim's g g (go to top) are supported:

useAction('nav:top', {
  label: 'Go to top',
  defaultBindings: ['g g'],  // Press g, then g again
  handler: () => scrollToTop(),
})

The SequenceModal shows available completions while typing a sequence.

Key Aliases

For convenience, common key names have shorter aliases:

| Alias | Key | |-------|-----| | left, right, up, down | Arrow keys | | esc | escape | | del | delete | | return | enter | | pgup, pgdn | pageup, pagedown |

useAction('nav:prev', {
  label: 'Previous item',
  defaultBindings: ['left', 'h'],  // 'left' = 'arrowleft'
  handler: () => selectPrev(),
})

User Customization

Users can edit bindings in the ShortcutsModal. Changes persist to localStorage using the storageKey you provide.

Components

<HotkeysProvider>

Wrap your app to enable the hotkeys system:

<HotkeysProvider config={{
  storageKey: 'use-kbd',      // localStorage key for user overrides (default)
  sequenceTimeout: Infinity,  // ms before sequence times out (default: no timeout)
  disableConflicts: false,    // Disable keys with multiple actions (default: false)
  enableOnTouch: false,       // Enable hotkeys on touch devices (default: false)
}}>
  {children}
</HotkeysProvider>

Note: Modal/omnibar trigger bindings are configured via component props (defaultBinding), not provider config.

<ShortcutsModal>

Displays all registered actions grouped by category. Users can click bindings to edit them on desktop.

<ShortcutsModal groups={[
  { id: 'nav', label: 'Navigation' },
  { id: 'edit', label: 'Editing' },
]} />

<Omnibar>

Command palette for searching and executing actions:

<Omnibar
  placeholder="Type a command..."
  maxResults={10}
/>

<LookupModal>

Browse and filter shortcuts by typing key sequences. Press ⌘⇧K (default) to open.

<LookupModal defaultBinding="meta+shift+k" />

Open programmatically with pre-filled keys via context:

const { openLookup } = useHotkeysContext()

// Open with "g" already typed (shows all "g ..." sequences)
openLookup([{ key: 'g', modifiers: { ctrl: false, alt: false, shift: false, meta: false } }])

<SequenceModal>

Shows pending keys and available completions during sequence input. No props needed—it reads from context.

<SequenceModal />

Styling

Import the default styles:

import 'use-kbd/styles.css'

Customize with CSS variables:

.kbd-modal,
.kbd-omnibar,
.kbd-sequence {
  --kbd-bg: #1f2937;
  --kbd-text: #f3f4f6;
  --kbd-border: #4b5563;
  --kbd-accent: #3b82f6;
  --kbd-kbd-bg: #374151;
}

Dark mode is automatically applied via [data-theme="dark"] or .dark selectors.

See awair's use-kbd-demo branch for a real-world integration example.

Mobile Support

While keyboard shortcuts are primarily a desktop feature, use-kbd provides solid mobile UX out of the box. Try the demos on your phone →

What works on mobile:

  • Omnibar search – Tap the search icon or ⌘K badge to open, then search and execute actions
  • LookupModal – Browse shortcuts by typing on the virtual keyboard
  • ShortcutsModal – View all available shortcuts (editing disabled since there's no physical keyboard)
  • Back button/swipe – Native gesture closes modals
  • Responsive layouts – All components adapt to small screens

Demo-specific features:

  • Table demo – Tap search icon in the floating controls to open omnibar
  • Canvas demo – Touch-to-draw support alongside keyboard shortcuts

For apps that want keyboard shortcuts on desktop but still need the omnibar/search on mobile, this covers the common case without extra configuration.

Patterns

ActionLink

Make navigation links discoverable in the omnibar by registering them as actions. Here's a reference implementation for react-router:

import { useEffect, useRef } from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { useMaybeHotkeysContext } from 'use-kbd'

interface ActionLinkProps {
  to: string
  label?: string
  group?: string
  keywords?: string[]
  defaultBinding?: string
  children: React.ReactNode
}

export function ActionLink({
  to,
  label,
  group = 'Navigation',
  keywords,
  defaultBinding,
  children,
}: ActionLinkProps) {
  const ctx = useMaybeHotkeysContext()
  const navigate = useNavigate()
  const location = useLocation()

  const isActive = location.pathname === to
  const effectiveLabel = label ?? (typeof children === 'string' ? children : to)
  const actionId = `nav:${to}`

  // Use ref to avoid re-registration on navigate change
  const navigateRef = useRef(navigate)
  navigateRef.current = navigate

  useEffect(() => {
    if (!ctx?.registry) return

    ctx.registry.register(actionId, {
      label: effectiveLabel,
      group,
      keywords,
      defaultBindings: defaultBinding ? [defaultBinding] : [],
      handler: () => navigateRef.current(to),
      enabled: !isActive,  // Hide from omnibar when on current page
    })

    return () => ctx.registry.unregister(actionId)
  }, [ctx?.registry, actionId, effectiveLabel, group, keywords, defaultBinding, isActive, to])

  return <Link to={to}>{children}</Link>
}

Usage:

<ActionLink to="/docs" keywords={['help', 'guide']}>Documentation</ActionLink>
<ActionLink to="/settings" defaultBinding="g s">Settings</ActionLink>

Adapt for Next.js, TanStack Router, or other routers by swapping the router hooks.

Low-Level Hooks

For advanced use cases, the underlying hooks are also exported:

useHotkeys(keymap, handlers, options?)

Register shortcuts directly without the provider:

useHotkeys(
  { 't': 'setTemp', 'meta+s': 'save' },
  { setTemp: () => setMetric('temp'), save: handleSave }
)

useRecordHotkey(options?)

Capture key combinations from user input:

const { isRecording, startRecording, display } = useRecordHotkey({
  onCapture: (sequence, display) => saveBinding(display.id),
})

useEditableHotkeys(defaults, handlers, options?)

Wraps useHotkeys with localStorage persistence and conflict detection.

Inspiration

  • macOS and GDrive menu search
  • Superhuman omnibar
  • Vimium keyboard-driven browsing
  • Android searchable settings

License

MIT