ComponentsNavigation

Keyboard shortcuts sheet

A side sheet listing every shortcut, grouped by where it works. Press ? or ⌘ / from anywhere in the workspace, or click Keyboard shortcuts in the sidebar.

Playground

One shortcut row. Change the controls to try it; the code under the preview lists only the props you changed.

Log activity
⌘E
import { ShortcutRow } from '@/ui/keyboard-shortcuts-sheet';<ShortcutRow label="Log activity" keys={["Mod","E"]} />

Sheet

400px from the right with the overlay shadow; the page stays visible behind it. Groups follow the shortcut registry's order. Each row says what it does on the left and shows its keys on the right, alternatives joined by "or". Keys print for the platform: ⌘ on a Mac, Ctrl elsewhere. The list scrolls under the fixed title.

import { useState } from 'react';
import { Button } from '@/ui/button';
import { KeyboardShortcutsSheet } from '@/ui/keyboard-shortcuts-sheet';

const groups = [
  {
    label: 'Navigation',
    shortcuts: [
      { label: 'Ask Flo or search, go to a page or create', keys: ['Mod', 'K'], or: ['/'] },
      { label: 'Show keyboard shortcuts', keys: ['?'], or: ['Mod', '/'] },
      { label: 'Show or hide the sidebar', keys: ['Mod', 'B'] },
      { label: 'Log activity', keys: ['Mod', 'E'] },
    ],
  },
  {
    label: 'Table',
    shortcuts: [
      { label: 'Edit the cell, or open what it links to', keys: ['Enter'], or: ['F2'] },
      { label: 'Select the row', keys: ['Shift', 'Space'] },
      { label: 'Cancel the edit', keys: ['Esc'] },
    ],
  },
  { label: 'Forms', shortcuts: [{ label: 'Save the form', keys: ['Mod', 'Enter'] }] },
];

// In the app ? or ⌘ / opens it from anywhere outside a text field; here the button does.
export default function KeyboardShortcutsSheetDefault() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button onClick={() => setOpen(true)}>Keyboard shortcuts</Button>
      <KeyboardShortcutsSheet open={open} onOpenChange={setOpen} groups={groups} />
    </>
  );
}

When things go wrong

A long description wraps beside its keys, which never wrap. An empty registry shows one line instead of a blank sheet.

import { useState } from 'react';
import { Button } from '@/ui/button';
import { KeyboardShortcutsSheet } from '@/ui/keyboard-shortcuts-sheet';

const long = [
  {
    label: 'Reviewing people',
    shortcuts: [
      { label: 'Approve the proposed change for Nguyễn Thị Minh Khai and move to the next person in the review queue', keys: ['Y'], or: ['Mod', 'Enter'] },
      { label: 'Deny it', keys: ['N'] },
    ],
  },
];

// A long description wraps beside keys that never wrap. An empty registry says so instead of showing a blank sheet.
export default function KeyboardShortcutsSheetUnhappy() {
  const [open, setOpen] = useState<'long' | 'empty' | null>(null);
  return (
    <div className="flex gap-100">
      <Button onClick={() => setOpen('long')}>Long description</Button>
      <Button onClick={() => setOpen('empty')}>Empty registry</Button>
      <KeyboardShortcutsSheet open={open !== null} onOpenChange={(next) => !next && setOpen(null)} groups={open === 'long' ? long : []} />
    </div>
  );
}

Motion

The sheet uses the Sheet motion, as in the app (src/components/shortcuts-sheet.tsx is a side sheet). Press Replay to close and open it again.

import { useState } from 'react';
import { Button } from '@/ui/button';
import { KeyboardShortcutsSheet } from '@/ui/keyboard-shortcuts-sheet';

const groups = [{ label: 'Navigation', shortcuts: [{ label: 'Show or hide the sidebar', keys: ['Mod', 'B'] }] }];

// Replay closes the sheet, then opens it again, so the 180ms exit and the 250ms entry run back to back.
export default function KeyboardShortcutsSheetMotion() {
  const [open, setOpen] = useState(false);
  const replay = () => {
    setOpen(false);
    setTimeout(() => setOpen(true), 500);
  };
  return (
    <>
      <Button variant="ghost" onClick={replay}>
        Replay
      </Button>
      <KeyboardShortcutsSheet open={open} onOpenChange={setOpen} groups={groups} />
    </>
  );
}

On this page