# Shortcut Cheatsheet — React Command Palette

> A keyboard shortcut overlay with physical-looking key caps that press down in sequence, on a demo loop, on hover, or when you type the combo.

- Category: [Command & Search](https://pixelpapa.com/components/command.md)
- Tags: command, shortcuts, keyboard, overlay, keycaps
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/command/command-shortcut-cheatsheet

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/command-shortcut-cheatsheet.json"
```

The file is written to `components/pixelpapa/command-shortcut-cheatsheet.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { Search } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import { useEffect, useState, type KeyboardEvent } from "react";
import { cn } from "@/lib/utils";

export type Shortcut = { label: string; keys: string[] };
export type ShortcutGroup = { title: string; shortcuts: Shortcut[] };

export function KeyCap({ children, pressed = false, delay = 0 }: { children: string; pressed?: boolean; delay?: number }) {
  const reduce = useReducedMotion();
  return (
    <span className="relative inline-block h-7 min-w-7 rounded-[7px] bg-border pb-[3px]">
      <motion.kbd
        animate={pressed && !reduce ? { y: 3 } : { y: 0 }}
        transition={{ type: "spring", stiffness: 700, damping: 26, delay: pressed ? delay : 0 }}
        className={cn(
          "flex h-full min-w-7 items-center justify-center rounded-[7px] border px-1.5 font-sans text-[12px] font-medium transition-colors duration-150",
          pressed ? "border-primary bg-primary text-primary-foreground" : "border-border bg-card text-foreground",
        )}
      >
        {children}
      </motion.kbd>
    </span>
  );
}

const SYMBOLS: Record<string, string> = { Meta: "⌘", Control: "⌃", Shift: "⇧", Alt: "⌥", Enter: "↵", Escape: "esc", ArrowUp: "↑", ArrowDown: "↓" };

type ShortcutCheatsheetProps = {
  groups: ShortcutGroup[];
  /** Cycle through the shortcuts, pressing each in turn. */
  autoplay?: boolean;
  className?: string;
};

export function ShortcutCheatsheet({ groups, autoplay = true, className }: ShortcutCheatsheetProps) {
  const [query, setQuery] = useState("");
  const [hover, setHover] = useState<string | null>(null);
  const [held, setHeld] = useState<string[]>([]);
  const [tour, setTour] = useState(0);
  const all = groups.flatMap((g) => g.shortcuts);
  const idle = hover === null && held.length === 0 && !query;

  useEffect(() => {
    if (!autoplay || !idle) return;
    const id = setInterval(() => setTour((t) => t + 1), 1500);
    return () => clearInterval(id);
  }, [autoplay, idle]);

  const typed = all.find((s) => s.keys.length === held.length && s.keys.every((k) => held.includes(k)));
  const lit = typed?.label ?? hover ?? (idle && autoplay ? all[tour % all.length]?.label : null);
  const q = query.trim().toLowerCase();

  function onKeyDown(e: KeyboardEvent<HTMLDivElement>) {
    if ((e.target as HTMLElement).tagName === "INPUT") return;
    const k = SYMBOLS[e.key] ?? e.key.toUpperCase();
    setHeld((h) => (h.includes(k) ? h : [...h, k]));
  }

  return (
    <div
      tabIndex={0}
      role="group"
      aria-label="Keyboard shortcuts. Press a combination to highlight it."
      onKeyDown={onKeyDown}
      onKeyUp={() => setHeld([])}
      onBlur={() => setHeld([])}
      className={cn("w-full rounded-2xl border border-border bg-popover shadow-2xl shadow-primary/10 outline-none focus-visible:ring-2 focus-visible:ring-ring", className)}
    >
      <div className="flex items-center justify-between gap-4 border-b border-border px-5 py-3.5">
        <p className="flex items-center gap-2.5 font-medium text-popover-foreground">
          Keyboard shortcuts <KeyCap>?</KeyCap>
        </p>
        <label className="flex h-8 w-44 items-center gap-2 rounded-lg border border-border bg-background px-2.5 focus-within:ring-2 focus-within:ring-ring">
          <Search className="size-3.5 text-muted-foreground" />
          <input
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Filter"
            aria-label="Filter shortcuts"
            className="w-full bg-transparent text-xs text-foreground outline-none placeholder:text-muted-foreground"
          />
        </label>
      </div>

      <div className="grid gap-x-8 gap-y-4 p-5 sm:grid-cols-2">
        {groups.map((g) => {
          const rows = g.shortcuts.filter((s) => !q || s.label.toLowerCase().includes(q));
          return (
            <section key={g.title} className={cn(rows.length === 0 && "opacity-40")}>
              <h3 className="mb-1 text-[11px] font-medium tracking-wide text-muted-foreground uppercase">{g.title}</h3>
              <ul>
                {rows.map((s) => {
                  const on = lit === s.label;
                  return (
                    <li
                      key={s.label}
                      onPointerEnter={() => setHover(s.label)}
                      onPointerLeave={() => setHover(null)}
                      className={cn("-mx-2 flex h-10 items-center justify-between gap-3 rounded-lg px-2 text-sm transition-colors", on ? "bg-accent text-accent-foreground" : "text-popover-foreground")}
                    >
                      <span className="truncate">{s.label}</span>
                      <span className="flex shrink-0 gap-1">
                        {s.keys.map((k, i) => (
                          <KeyCap key={k} pressed={on} delay={i * 0.09}>
                            {k}
                          </KeyCap>
                        ))}
                      </span>
                    </li>
                  );
                })}
                {rows.length === 0 && <li className="flex h-10 items-center text-xs text-muted-foreground">No matches</li>}
              </ul>
            </section>
          );
        })}
      </div>
    </div>
  );
}

const groups: ShortcutGroup[] = [
  {
    title: "General",
    shortcuts: [
      { label: "Open command palette", keys: ["⌘", "K"] },
      { label: "Search everywhere", keys: ["⌘", "⇧", "F"] },
      { label: "Toggle sidebar", keys: ["⌘", "B"] },
      { label: "Show this cheatsheet", keys: ["?"] },
    ],
  },
  {
    title: "Editing",
    shortcuts: [
      { label: "Duplicate block", keys: ["⌘", "D"] },
      { label: "Move block up", keys: ["⌥", "↑"] },
      { label: "Turn into heading", keys: ["⌘", "⌥", "1"] },
      { label: "Add comment", keys: ["⌘", "⇧", "M"] },
    ],
  },
  {
    title: "Navigation",
    shortcuts: [
      { label: "Go to inbox", keys: ["G", "I"] },
      { label: "Go to documents", keys: ["G", "D"] },
    ],
  },
  {
    title: "Review",
    shortcuts: [
      { label: "Approve change", keys: ["⌘", "↵"] },
      { label: "Next suggestion", keys: ["J"] },
    ],
  },
];

export default function CommandShortcutCheatsheetDemo() {
  return <ShortcutCheatsheet groups={groups} className="max-w-xl" />;
}
```

## AI prompt

```text
Add the "Shortcut Cheatsheet" component from PixelPapa UI to this project.

What it is: A keyboard shortcut overlay with physical-looking key caps that press down in sequence, on a demo loop, on hover, or when you type the combo.
Category: Command & Search. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/command-shortcut-cheatsheet.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/command-shortcut-cheatsheet.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/command-shortcut-cheatsheet" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More command & search

- [Command Palette](https://pixelpapa.com/components/command/command-palette.md): A grouped command palette with fuzzy matching that highlights matched letters, a highlight that glides between rows on arrow keys, recents and footer hints.
- [Inline Slash Menu](https://pixelpapa.com/components/command/command-inline-slash-menu.md): Type "/" on an editor line to open a block menu that filters as you type, moves with the arrow keys and turns the line into the chosen block.
- [Spotlight Search](https://pixelpapa.com/components/command/command-spotlight-search.md) (Pro): A large centred search field with a results column and a preview pane that crossfades to the highlighted result as you arrow through.
