Skip to content

Repository files navigation

@nobertdev/react-spotlight-search

A beautiful, accessible command palette / spotlight search for React apps. Cmd+K ready.

npm version license Buy Me a Coffee


react-spotlight-search demo

🚀 Live Demo →


Features

  • ⌨️ Keyboard-firstCmd+K / Ctrl+K out of the box, fully navigable with arrow keys
  • Action shortcuts — each action's shortcut field registers as a real global keybinding, not just a label
  • 🔍 Fuzzy search — smart weighted ranking across label, description, and keywords
  • 🗂️ Grouped actions — organise actions under named categories
  • 🌗 Light / Dark / Auto theme — respects system preference and Tailwind's dark class
  • Accessiblerole="dialog", role="combobox", full keyboard navigation, screen-reader friendly
  • 🪶 Zero styling dependencies — styles are injected automatically; no CSS import needed
  • 🪟 Portal rendering — modal renders on document.body, avoiding z-index conflicts
  • 📦 Tiny — ~4 kb gzipped, only React as a peer dependency
  • 🔷 Fully typed — written in TypeScript with all types exported

Table of Contents


Installation

npm install @nobertdev/react-spotlight-search
# or
yarn add @nobertdev/react-spotlight-search
# or
pnpm add @nobertdev/react-spotlight-search

Peer dependencies — ensure these are already in your project:

npm install react react-dom   # React >=17 required

Quick Start

1. Wrap your app with SpotlightProvider

import { SpotlightProvider } from "@nobertdev/react-spotlight-search";

const actions = [
  {
    id: "home",
    label: "Go to Home",
    description: "Navigate to the home page",
    icon: "🏠",
    group: "Navigation",
    onSelect: () => (window.location.href = "/"),
  },
  {
    id: "dark-mode",
    label: "Toggle Dark Mode",
    description: "Switch between light and dark themes",
    icon: "🌙",
    shortcut: "⌘D",
    keywords: ["theme", "appearance", "dark", "light"],
    group: "Settings",
    onSelect: () => document.documentElement.classList.toggle("dark"),
  },
];

export default function App() {
  return (
    <SpotlightProvider actions={actions} theme="auto">
      <YourApp />
    </SpotlightProvider>
  );
}

Press Cmd+K (macOS) or Ctrl+K (Windows / Linux) to open the palette.


2. Add a trigger button

import { useSpotlightContext } from "@nobertdev/react-spotlight-search";

function NavBar() {
  const { open } = useSpotlightContext();

  return (
    <button onClick={open}>
      Search <kbd>⌘K</kbd>
    </button>
  );
}

3. Standalone usage (no provider)

Use useSpotlight to manage state yourself and render <Spotlight> directly:

import { Spotlight, useSpotlight } from "@nobertdev/react-spotlight-search";
import { createPortal } from "react-dom";

function MyApp() {
  const { isOpen, close } = useSpotlight({ shortcut: "mod+k" });

  return (
    <>
      {isOpen &&
        createPortal(
          <Spotlight actions={actions} onClose={close} />,
          document.body,
        )}
    </>
  );
}

API Reference

SpotlightProvider

Top-level provider component. Manages state, registers the keyboard shortcut, and renders the modal as a portal.

<SpotlightProvider
  actions={actions}
  shortcut="mod+k"
  theme="auto"
  placeholder="Search actions..."
  emptyMessage="No results found."
  limit={8}
  onOpen={() => console.log("opened")}
  onClose={() => console.log("closed")}
>
  <App />
</SpotlightProvider>
Prop Type Default Description
children React.ReactNode Required. Your app content.
actions SpotlightAction[] [] Initial list of actions shown in the palette.
shortcut string "mod+k" Keyboard shortcut to open/close the palette.
placeholder string "Search actions..." Placeholder text inside the search input.
emptyMessage string "No results found." Message displayed when no results match the query.
limit number 8 Maximum number of results shown at a time.
theme "light" | "dark" | "auto" "auto" Colour scheme. "auto" follows system / Tailwind class.
className string Extra CSS class applied to the overlay element.
onOpen () => void Callback fired when the palette opens.
onClose () => void Callback fired when the palette closes.

Spotlight

The bare modal component. Renders the overlay, search input, and action list. Use this when you want complete manual control over open/close state.

<Spotlight
  actions={actions}
  onClose={close}
  placeholder="Search..."
  emptyMessage="Nothing found."
  limit={10}
  theme="dark"
/>

Accepts all props from SpotlightProvider except children and shortcut.

Note: <Spotlight> does not register any keyboard shortcut on its own. Use useSpotlight alongside it to add shortcut support.


useSpotlight

A hook that binds a keyboard shortcut and returns open/close/toggle handlers. This is the building block used internally by SpotlightProvider.

const { isOpen, open, close, toggle } = useSpotlight(options?);

Options

Option Type Default Description
shortcut string "mod+k" Key combination (see Shortcut Format).
onOpen () => void Fired when the state transitions to open.
onClose () => void Fired when the state transitions to closed.

Return value

Key Type Description
isOpen boolean Whether the palette is currently open.
open () => void Programmatically open the palette.
close () => void Programmatically close the palette.
toggle () => void Toggle between open and closed.

Example

const { isOpen, open, close } = useSpotlight({
  shortcut: "mod+shift+p",
  onOpen: () => analytics.track("spotlight_opened"),
});

useSpotlightContext

Access the spotlight state and actions from anywhere inside a <SpotlightProvider> tree.

const { isOpen, open, close, toggle, setActions } = useSpotlightContext();
Key Type Description
isOpen boolean Whether the palette is currently open.
open () => void Open the palette.
close () => void Close the palette.
toggle () => void Toggle the palette.
setActions (actions: SpotlightAction[]) => void Replace the current action list at runtime.

Throws an error if called outside of <SpotlightProvider>.


SpotlightAction

The shape of each item in the palette.

Field Type Required Description
id string Yes Unique identifier for the action.
label string Yes Primary text displayed in the list.
onSelect () => void Yes Called when the action is activated (click or Enter).
description string No Secondary line of text shown below the label.
icon React.ReactNode No Icon rendered to the left of the label (emoji or component).
shortcut string No Keyboard shortcut for this action (e.g. "⌘S", "mod+shift+s"). Displayed on the right of the row and registered as a real global keybinding. Fires onSelect when pressed, even when the palette is closed. Does not fire while the user is typing in an input or textarea.
group string No Category name used to visually group related actions.
keywords string[] No Additional search terms to improve fuzzy-match discoverability.

Shortcut Format

Shortcuts are strings composed of modifier names joined by +. The last segment is the key character (case-insensitive).

Token Maps to
mod Cmd on macOS, Ctrl on Windows/Linux
ctrl Ctrl
cmd Cmd (macOS)
shift Shift
alt Alt / Option
"mod+k"          →  Cmd/Ctrl + K          (default)
"mod+shift+p"    →  Cmd/Ctrl + Shift + P
"alt+space"      →  Alt + Space
"ctrl+shift+f"   →  Ctrl + Shift + F

Unicode symbols are also accepted on action-level shortcut fields:

"⌘K"    →  Cmd/Ctrl + K
"⌘⇧P"   →  Cmd/Ctrl + Shift + P
"⌃S"    →  Ctrl + S
"⌥T"    →  Alt + T

Action Shortcuts

Every action with a shortcut field has that shortcut automatically registered as a real global keyboard listener by <SpotlightProvider>. Pressing the shortcut calls the action's onSelect directly — the palette does not need to be open.

const actions = [
  {
    id: "save",
    label: "Save Document",
    shortcut: "⌘S", // rendered as a badge AND bound as ⌘/Ctrl+S
    onSelect: () => save(),
  },
  {
    id: "format",
    label: "Format Code",
    shortcut: "mod+shift+f", // text notation works too
    onSelect: () => format(),
  },
];

Behaviour notes:

  • Shortcuts are re-registered whenever the actions array changes.
  • The listener is skipped when an <input>, <textarea>, or contentEditable element has focus, so typing in the search box (or any form field) will never trigger an action shortcut.
  • / mod resolve to Cmd on macOS and Ctrl on Windows / Linux.

Keyboard Controls

Key Action
Move selection down.
Move selection up.
Enter Activate the highlighted action.
Escape Close the palette.
Click outside Close the palette.

Theming

Pass theme to <SpotlightProvider> or <Spotlight>:

// Always use light theme
<SpotlightProvider theme="light" actions={actions}></SpotlightProvider>

// Always use dark theme
<SpotlightProvider theme="dark" actions={actions}></SpotlightProvider>

// Follow system preference or class-based theme (default)
<SpotlightProvider theme="auto" actions={actions}></SpotlightProvider>

"auto" mode

When theme="auto", the component resolves the active theme in this priority order:

  1. A dark or light class on <html> (set by Tailwind CSS, shadcn/ui, next-themes, etc.)
  2. The OS-level prefers-color-scheme media query.

Both sources are watched reactively — the palette updates instantly if the theme changes while it is open.


Usage Patterns

Actions with icons and groups

import { HomeIcon, SettingsIcon, UserIcon } from "lucide-react";

const actions = [
  {
    id: "home",
    label: "Home",
    icon: <HomeIcon size={16} />,
    group: "Navigation",
    onSelect: () => router.push("/"),
  },
  {
    id: "settings",
    label: "Settings",
    description: "Manage your account settings",
    icon: <SettingsIcon size={16} />,
    group: "Navigation",
    shortcut: "⌘,",
    onSelect: () => router.push("/settings"),
  },
  {
    id: "profile",
    label: "Profile",
    icon: <UserIcon size={16} />,
    group: "Navigation",
    shortcut: "⌘P",
    keywords: ["account", "avatar", "me"],
    onSelect: () => router.push("/profile"),
  },
];

Dynamically replacing actions

Use setActions to swap the action list at runtime — useful for context-sensitive palettes:

function Editor() {
  const { setActions, open } = useSpotlightContext();

  useEffect(() => {
    setActions([
      { id: "format", label: "Format Document", onSelect: formatDoc },
      { id: "save", label: "Save", shortcut: "⌘S", onSelect: save },
      { id: "close", label: "Close File", onSelect: closeFile },
    ]);
  }, []);

  return <button onClick={open}>Editor Commands</button>;
}

Custom open shortcut

<SpotlightProvider actions={actions} shortcut="mod+shift+p">
  <App />
</SpotlightProvider>

Responding to open/close events

<SpotlightProvider
  actions={actions}
  onOpen={() => analytics.track("palette_opened")}
  onClose={() => analytics.track("palette_closed")}
>
  <App />
</SpotlightProvider>

TypeScript

All types are exported from the package:

import type {
  SpotlightAction,
  SpotlightProps,
  UseSpotlightOptions,
  UseSpotlightReturn,
} from "@nobertdev/react-spotlight-search";

SpotlightAction

interface SpotlightAction {
  id: string;
  label: string;
  description?: string;
  icon?: React.ReactNode;
  shortcut?: string;
  group?: string;
  keywords?: string[];
  onSelect: () => void;
}

UseSpotlightOptions

interface UseSpotlightOptions {
  shortcut?: string;
  onOpen?: () => void;
  onClose?: () => void;
}

UseSpotlightReturn

interface UseSpotlightReturn {
  isOpen: boolean;
  open: () => void;
  close: () => void;
  toggle: () => void;
}

License

MIT © Nobert Langat


Support

If this package saves you time, consider buying me a coffee — it helps keep the work going!

Buy Me a Coffee

About

A beautiful, accessible command palette / spotlight search for React apps. Cmd+K ready.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages