npm package: @nobertdev/react-spotlight-search
Description: A beautiful, accessible command palette / spotlight search for React apps. Cmd+K ready.
Author: Nobert Langat
Latest version: 0.2.0

# @nobertdev/react-spotlight-search

> A beautiful, accessible command palette / spotlight search for React apps. Cmd+K ready.

[![npm version](https://img.shields.io/npm/v/@nobertdev/react-spotlight-search.svg)](https://www.npmjs.com/package/@nobertdev/react-spotlight-search)
[![license](https://img.shields.io/npm/l/@nobertdev/react-spotlight-search)](LICENSE)
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-yellow?logo=buy-me-a-coffee)](https://buymeacoffee.com/nobertdev)

---

<p align="center">
  <a href="https://react-spotlight-search-demo-sbe6.vercel.app/" target="_blank">
    <img
      src="./demo.gif"
      alt="react-spotlight-search demo"
      width="640"
      style="border-radius: 12px"
    />
  </a>
</p>

<p align="center">
  <a href="https://react-spotlight-search-demo-sbe6.vercel.app/" target="_blank">
    <strong>🚀 Live Demo →</strong>
  </a>
</p>

---

## Features

- ⌨️ **Keyboard-first** — `Cmd+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
- ♿ **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](#installation)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
  - [SpotlightProvider](#spotlightprovider)
  - [Spotlight](#spotlight)
  - [useSpotlight](#usespotlight)
  - [useSpotlightContext](#usespotlightcontext)
  - [SpotlightAction](#spotlightaction)
- [Shortcut Format](#shortcut-format)
- [Action Shortcuts](#action-shortcuts)
- [Keyboard Controls](#keyboard-controls)
- [Theming](#theming)
- [Usage Patterns](#usage-patterns)
- [TypeScript](#typescript)
- [License](#license)
- [Support](#support)

---

## Installation

```bash
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:

```bash
npm install react react-dom   # React >=17 required
```

---

## Quick Start

### 1. Wrap your app with `SpotlightProvider`

```tsx
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

```tsx
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:

```tsx
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.

```tsx
<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.

```tsx
<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`.

```ts
const { isOpen, open, close, toggle } = useSpotlight(options?);
```

**Options**

| Option     | Type         | Default   | Description                                                |
| ---------- | ------------ | --------- | ---------------------------------------------------------- |
| `shortcut` | `string`     | `"mod+k"` | Key combination (see [Shortcut Format](#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**

```tsx
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.

```ts
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.

```tsx
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>`:

```tsx
// 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

```tsx
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:

```tsx
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

```tsx
<SpotlightProvider actions={actions} shortcut="mod+shift+p">
  <App />
</SpotlightProvider>
```

### Responding to open/close events

```tsx
<SpotlightProvider
  actions={actions}
  onOpen={() => analytics.track("palette_opened")}
  onClose={() => analytics.track("palette_closed")}
>
  <App />
</SpotlightProvider>
```

---

## TypeScript

All types are exported from the package:

```ts
import type {
  SpotlightAction,
  SpotlightProps,
  UseSpotlightOptions,
  UseSpotlightReturn,
} from "@nobertdev/react-spotlight-search";
```

### `SpotlightAction`

```ts
interface SpotlightAction {
  id: string;
  label: string;
  description?: string;
  icon?: React.ReactNode;
  shortcut?: string;
  group?: string;
  keywords?: string[];
  onSelect: () => void;
}
```

### `UseSpotlightOptions`

```ts
interface UseSpotlightOptions {
  shortcut?: string;
  onOpen?: () => void;
  onClose?: () => void;
}
```

### `UseSpotlightReturn`

```ts
interface UseSpotlightReturn {
  isOpen: boolean;
  open: () => void;
  close: () => void;
  toggle: () => void;
}
```

---

## License

MIT © [Nobert Langat](https://nobertdev.vercel.app/)

---

## Support

If this package saves you time, consider buying me a coffee — it helps keep the work going!

[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-yellow?logo=buy-me-a-coffee&style=for-the-badge)](https://buymeacoffee.com/nobertdev)