@nobertdev/react-spotlight-search
v0.2.0
Published
A beautiful, accessible command palette / spotlight search for React apps. Cmd+K ready.
Maintainers
Readme
@nobertdev/react-spotlight-search
A beautiful, accessible command palette / spotlight search for React apps. Cmd+K ready.
Features
- ⌨️ 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
Table of Contents
- Installation
- Quick Start
- API Reference
- Shortcut Format
- Action Shortcuts
- Keyboard Controls
- Theming
- Usage Patterns
- TypeScript
- License
- Support
Installation
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 requiredQuick 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. UseuseSpotlightalongside 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 + FUnicode symbols are also accepted on action-level shortcut fields:
"⌘K" → Cmd/Ctrl + K
"⌘⇧P" → Cmd/Ctrl + Shift + P
"⌃S" → Ctrl + S
"⌥T" → Alt + TAction 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
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.
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:
- 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.
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!
