A beautiful, accessible command palette / spotlight search for React apps. Cmd+K ready.
- ⌨️ Keyboard-first —
Cmd+K/Ctrl+Kout of the box, fully navigable with arrow keys - ⚡ Action shortcuts — each action's
shortcutfield 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
darkclass - ♿ Accessible —
role="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
- Installation
- Quick Start
- API Reference
- Shortcut Format
- Action Shortcuts
- Keyboard Controls
- Theming
- Usage Patterns
- TypeScript
- License
- Support
npm install @nobertdev/react-spotlight-search
# or
yarn add @nobertdev/react-spotlight-search
# or
pnpm add @nobertdev/react-spotlight-searchPeer dependencies — ensure these are already in your project:
npm install react react-dom # React >=17 requiredimport { 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.
import { useSpotlightContext } from "@nobertdev/react-spotlight-search";
function NavBar() {
const { open } = useSpotlightContext();
return (
<button onClick={open}>
Search <kbd>⌘K</kbd>
</button>
);
}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,
)}
</>
);
}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. |
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. UseuseSpotlightalongside it to add shortcut support.
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"),
});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>.
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. |
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
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
actionsarray changes. - The listener is skipped when an
<input>,<textarea>, orcontentEditableelement has focus, so typing in the search box (or any form field) will never trigger an action shortcut. ⌘/modresolve toCmdon macOS andCtrlon Windows / Linux.
| Key | Action |
|---|---|
↓ |
Move selection down. |
↑ |
Move selection up. |
Enter |
Activate the highlighted action. |
Escape |
Close the palette. |
| Click outside | Close the palette. |
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>When theme="auto", the component resolves the active theme in this priority order:
- A
darkorlightclass on<html>(set by Tailwind CSS, shadcn/ui, next-themes, etc.) - The OS-level
prefers-color-schememedia query.
Both sources are watched reactively — the palette updates instantly if the theme changes while it is open.
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"),
},
];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>;
}<SpotlightProvider actions={actions} shortcut="mod+shift+p">
<App />
</SpotlightProvider><SpotlightProvider
actions={actions}
onOpen={() => analytics.track("palette_opened")}
onClose={() => analytics.track("palette_closed")}
>
<App />
</SpotlightProvider>All types are exported from the package:
import type {
SpotlightAction,
SpotlightProps,
UseSpotlightOptions,
UseSpotlightReturn,
} from "@nobertdev/react-spotlight-search";interface SpotlightAction {
id: string;
label: string;
description?: string;
icon?: React.ReactNode;
shortcut?: string;
group?: string;
keywords?: string[];
onSelect: () => void;
}interface UseSpotlightOptions {
shortcut?: string;
onOpen?: () => void;
onClose?: () => void;
}interface UseSpotlightReturn {
isOpen: boolean;
open: () => void;
close: () => void;
toggle: () => void;
}MIT © Nobert Langat
If this package saves you time, consider buying me a coffee — it helps keep the work going!