# Shortcuts Dialog

> A React keyboard shortcuts dialog, the "?" overlay of keyboard-friendly apps: every shortcut grouped and searchable, in the device's notation.

- Type: React component (Overlays)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add shortcuts-dialog`
- Docs: https://wingo-ui.com/components/shortcuts-dialog

## Interaction

Press ? anywhere (not while typing) and the dialog springs in with every shortcut grouped in two columns; press it again to close. Keys show as ⌘ ⇧ glyphs on a Mac and Ctrl / Shift words elsewhere, alternatives read "⌘ K or /" and sequences "G then I". With a mouse or trackpad the search takes focus at once: type a word, a key ("ctrl", "shift", "⌘") or a group name and the rows narrow instantly with the matching letters underlined, groups without matches disappear and an empty state fades in when nothing is left; the card keeps its height while you search, so nothing jumps. Esc clears the search first, then closes; Tab moves between the search, the clear button and the close button. On a phone it is a tall bottom sheet in one column with the search pinned under the title (it keeps its height while you filter), opened from a trigger such as a help menu entry; swipe it down or tap outside to close, and the keyboard only appears when you tap the search.

## Installation

```bash
npx wingo-ui@latest add shortcuts-dialog
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/shortcuts-dialog` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/shortcuts-dialog.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))
- [Dialog](https://wingo-ui.com/components/dialog) ([Markdown](https://wingo-ui.com/components/dialog.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Kbd](https://wingo-ui.com/components/kbd) ([Markdown](https://wingo-ui.com/components/kbd.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Text match](https://wingo-ui.com/components/text-match) ([Markdown](https://wingo-ui.com/components/text-match.md))

## Usage

```tsx
import { Keyboard } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Kbd } from "@/components/ui/kbd"
import { ShortcutsDialog, type ShortcutGroup } from "@/components/ui/shortcuts-dialog"

const groups: ShortcutGroup[] = [
  {
    label: "General",
    items: [
      { label: "Search", shortcut: ["mod+k", "/"] },
      { label: "Shortcuts", shortcut: "?" },
      { label: "Toggle sidebar", shortcut: "mod+b" },
    ],
  },
  {
    label: "Navigation",
    items: [
      { label: "Go to Inbox", shortcut: ["g", "i"], sequence: true },
      { label: "Go to Clients", shortcut: ["g", "c"], sequence: true },
    ],
  },
  {
    label: "Invoices",
    items: [
      { label: "New invoice", shortcut: "f", keywords: ["issue"] },
      { label: "Send by email", shortcut: "mod+enter" },
      { label: "Download PDF", shortcut: "shift+mod+d" },
    ],
  },
]

export function HelpShortcuts() {
  return (
    <ShortcutsDialog
      groups={groups}
      title="Keyboard shortcuts"
      hint={
        <span className="inline-flex items-center gap-1.5">
          Press <Kbd shortcut="?" size="sm" /> any time to open the list
        </span>
      }
      trigger={
        <Button variant="soft" size="sm" leftIcon={<Keyboard />}>
          Shortcuts
        </Button>
      }
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `groups?` | `ShortcutGroup[]` | `"[]"` | { id?, label?: ReactNode, textValue?, items: ShortcutEntry[] }. An entry is { id?, label: ReactNode, shortcut: string \| string[], description?: ReactNode, sequence?: boolean, keywords?: string[], textValue?: string }. Shortcuts use mod+k notation; an array is alternatives ("mod+k" or "/"), or the keys of a sequence when sequence is true (["g", "i"]). An unparseable shortcut shows its raw text in one cap. Empty groups are hidden. |
| `title?` | `ReactNode` | `"Keyboard shortcuts"` | The dialog title. "" keeps it for screen readers only. |
| `description?` | `ReactNode` | `""` | A muted line under the title. |
| `searchPlaceholder?` | `string` | `"Search shortcuts"` | The search field's placeholder and accessible name. |
| `emptyText?` | `ReactNode` | `"No shortcuts found"` | Shown when nothing matches the search. |
| `thenLabel?` | `string` | `"then"` | Between the keys of a sequence ("G then I"). Screen readers read it too. |
| `orLabel?` | `string` | `"or"` | Between alternatives ("⌘ K or /"). |
| `closeLabel?` | `string` | `"Close"` | aria-label of the close button. |
| `clearLabel?` | `string` | `"Clear"` | aria-label of the search's clear button. |
| `hint?` | `ReactNode` |  | The footer line. Default: "Press ? anytime to see this list" with the bound key as a cap, hidden on touch screens; null hides it. Pass a translated node for localized apps. |
| `size?` | `"md" \| "lg" \| "xl"` | `"lg"` | The dialog's max width: 520 / 720 / 960px. On phones the sheet with a search is 92% tall, so filtering never resizes it; without a search it fits its content. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg"` | `"lg"` | Corner shape of the card (sm 12px, md 16px, lg 24px) and of the phone sheet's top corners. |
| `platform?` | `"auto" \| "apple" \| "other"` | `"auto"` | The key notation: apple shows ⌘ ⇧ ⌥ glyphs, other Ctrl / Shift / Alt words. auto renders the other form on the server, then the device's. |
| `kbdVariant?` | `"soft" \| "outline" \| "solid"` | `"soft"` | The key cap look: the quiet gray key, a bordered key or a raised physical key. |
| `kbdSize?` | `"sm" \| "md"` | `"sm"` | Key cap height, 16 or 20px. |
| `columns?` | `1 \| 2` | `2` | Groups side by side from 768px. Phones always get one column. |
| `defaultOpen?` | `boolean` | `false` | Starts open while uncontrolled. |
| `open?` | `boolean` |  | Controlled open state; pair it with onOpenChange (e.g. to open it from a help menu entry). |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when it opens or closes (trigger, shortcut, Esc, outside click, swipe). |
| `shortcut?` | `string` | `"?"` | Opens and closes it from anywhere, in mod+k notation. Keys without a modifier never fire while typing, so "?" in the search types a "?". "" binds nothing. The showcase itself listens for ?, so the demo binds ⌘/ in its place. |
| `search?` | `boolean` | `true` | A filter field above the groups. It matches labels, keywords, descriptions and the shortcut in both notations, ignoring case and diacritics; it takes focus on open only with a mouse or trackpad. |
| `responsive?` | `boolean` | `true` | A bottom sheet below 768px (one column, search pinned). false keeps the centered card on phones. |
| `trigger?` | `ReactNode` |  | The element that opens it, rendered with asChild (a single element that takes a ref); it gets aria-keyshortcuts. |
| `container?` | `HTMLElement \| null` |  | Portal target, document.body by default. |
| `classNames?` | `Partial<Record<"content" \| "header" \| "search" \| "groups" \| "group" \| "groupLabel" \| "row" \| "label" \| "description" \| "keys" \| "then" \| "or" \| "mark" \| "empty" \| "hint", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the dialog card (or the sheet), merged last. Other props (aria-*, data-*, style, onOpenAutoFocus, onEscapeKeyDown, ref) go to the same element. |

## Examples

- **Apple and other notation**: platform="apple" shows ⌘ ⇧ glyphs, "other" spells Ctrl and Shift; auto picks by device.
- **Small cheat sheet**: One column, no search, a medium dialog for a single screen's keys.
- **Descriptions on rows**: A muted line under a label explains what the key does; a custom hint in the footer.
- **Phone sheet**: What it becomes below 768px (a static replica of the sheet).
- **From a user menu**: Controlled open: a menu entry opens it, the way touch-only people reach it.
- **Outline keys, extra large**: kbdVariant="outline", kbdSize="md", size="xl", radius="md", and the Ctrl notation forced.

## See also

- [Kbd](https://wingo-ui.com/components/kbd) ([Markdown](https://wingo-ui.com/components/kbd.md))
- [Dialog](https://wingo-ui.com/components/dialog) ([Markdown](https://wingo-ui.com/components/dialog.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [Command Menu](https://wingo-ui.com/components/command-menu) ([Markdown](https://wingo-ui.com/components/command-menu.md))
- [Sheet](https://wingo-ui.com/components/sheet) ([Markdown](https://wingo-ui.com/components/sheet.md))

---

Canonical: https://wingo-ui.com/components/shortcuts-dialog
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
