Hotkeys
Keyboard shortcuts for desktop apps: one listener, scopes, guards for fields and open menus, conflict detection, remapping and a shortcuts sheet that can turn single-key shortcuts off (WCAG 2.1.4).
npx prodular-ui add hotkeysShortcuts and the reference sheet
Typing in the field never triggers single keys. Shift ? opens the sheet, where each shortcut can be changed and single keys turned off.
Press G (off), [ or Shift? outside the field.
import * as React from "react"import { KeyboardIcon } from "lucide-react"
import { Button } from "@/components/ui/button"import { HotkeyKeys, HotkeysProvider, ShortcutsDialog, useHotkey, useHotkeyKeys } from "@/components/ui/hotkeys"import { Input } from "@/components/ui/input"
function Screen() { const [log, setLog] = React.useState<string[]>([]) const [open, setOpen] = React.useState(false) const say = (s: string) => setLog((l) => [s, ...l].slice(0, 4))
useHotkey("mod+k", () => say("Command palette"), { id: "palette", description: "Open command palette", group: "General" }) useHotkey("shift+?", () => setOpen(true), { id: "help", description: "Show keyboard shortcuts", group: "General" }) useHotkey("g", () => say("Graph"), { id: "graph", description: "Focus the graph", group: "Views" }) useHotkey("l", () => say("List"), { id: "list", description: "Focus the list", group: "Views" }) useHotkey("[", () => say("Previous event"), { id: "prev", description: "Previous event", group: "Timeline" }) useHotkey("]", () => say("Next event"), { id: "next", description: "Next event", group: "Timeline" }) const graph = useHotkeyKeys("graph")
return ( <div className="flex w-full max-w-md flex-col gap-3"> <div className="flex items-center gap-2"> <Input aria-label="Notes" placeholder="Typing here never triggers G or L" /> <Button variant="outline" onClick={() => setOpen(true)}> <KeyboardIcon data-icon="inline-start" /> Shortcuts </Button> </div> <p className="text-(length:--text-ui) text-muted-foreground"> Press {graph ? <HotkeyKeys keys={graph} className="align-middle" /> : "G (off)"}, <HotkeyKeys keys="[" className="align-middle" /> or{" "} <HotkeyKeys keys="shift+?" className="align-middle" /> outside the field. </p> <ul aria-live="polite" className="flex min-h-24 flex-col gap-1 rounded-lg border bg-card p-3 text-sm"> {log.map((l, i) => ( <li key={i} className={i ? "text-muted-foreground" : ""}> {l} </li> ))} </ul> <ShortcutsDialog open={open} onOpenChange={setOpen} /> </div> )}
export default function HotkeysReference() { return ( <HotkeysProvider storageKey="prodular-demo-hotkeys"> <Screen /> </HotkeysProvider> )}Anatomy
<HotkeysProvider> <App /></HotkeysProvider>
useHotkey("mod+k", openPalette, { id: "palette", description: "Open command palette" })useHotkey("shift+?", () => setHelp(true), { id: "help", description: "Show keyboard shortcuts" })<ShortcutsDialog open={help} onOpenChange={setHelp} />Guidance
Use it when
- Apps people use all day, where the mouse slows them down.
- Single keys (G, [, ?) only for frequent actions, and always with the sheet so they can be turned off.
- useHotkeyScope for shortcuts that only make sense while a panel or editor has focus.
Avoid
- Reimplementing shortcuts with your own keydown listeners.
- Overriding browser and screen reader keys: Cmd or Ctrl plus L, T, W, R, F, and Tab.
- Shortcuts without a description; they cannot be found or changed.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Mount HotkeysProvider once near the root and register shortcuts with useHotkey(keys, handler, { id, description, group }).
- Write keys as "mod+k", "shift+?", "g" or "escape". mod is Cmd on macOS and Ctrl elsewhere.
- Always render ShortcutsDialog and open it with shift+?. It is how people discover, remap and turn off shortcuts.
- Show shortcuts in menus and tooltips with HotkeyKeys or useHotkeyKeys(id), so they reflect the person's remapping.
- Do not set allowInInputs on single-key shortcuts.
- Open CommandDialog from useHotkey("mod+k"), not from a separate listener.
API
HotkeysProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| storageKey | string | null | "prodular-ui:hotkeys" | Where remapping and the single-key switch are saved. null keeps them in memory. |
useHotkey
| Prop | Type | Default | Description |
|---|---|---|---|
| keys | string | "mod+k", "shift+?", "g", "[", "escape". | |
| options | { id, description, group, scope, enabled, allowInInputs, allowInOverlays, preventDefault } | id and description make it appear and remappable in the sheet. |
useHotkeyScope
| Prop | Type | Default | Description |
|---|---|---|---|
| (scope, active) | (string, boolean) => void | Turns a scope on while active. |
ShortcutsDialog
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | Controlled with onOpenChange. |
HotkeyKeys
| Prop | Type | Default | Description |
|---|---|---|---|
| keys | string | Renders the keys as Kbd, with the platform's symbols. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add hotkeys# or, with the registry in components.jsonnpx shadcn@latest add @prodular/hotkeys