Keyboard Shortcuts Sheet
A searchable keyboard-shortcut reference grouped by area; press ? to open it, and typing filters keys and names.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/shortcuts-sheet.json'use client'
import { useEffect, useRef, useState } from 'react'
import './shortcuts-sheet.css'
export interface ShortcutGroup { title: string; items: { label: string; keys: string[] }[] }
export interface ShortcutsSheetProps {
groups: ShortcutGroup[]
platform?: 'mac' | 'win'
fill?: 'dark' | 'light'
className?: string
}
const WIN: Record<string, string> = { '⌘': 'Ctrl', '⌥': 'Alt', '⇧': 'Shift', '⌃': 'Ctrl' }
const WORD: Record<string, string> = { '⌘': 'Command', '⌥': 'Option', '⇧': 'Shift', '⌃': 'Control', '↵': 'Enter', '⌫': 'Delete' }
/** Searchable keyboard-shortcut reference. */
export function ShortcutsSheet({ groups, platform = 'mac', fill = 'dark', className }: ShortcutsSheetProps) {
const [q, setQ] = useState('')
const [shown, setShown] = useState(true)
const input = useRef<HTMLInputElement>(null)
useEffect(() => {
const h = (e: KeyboardEvent) => {
const t = e.target as HTMLElement
if (e.key === '?' && !/INPUT|TEXTAREA/.test(t.tagName)) { e.preventDefault(); setShown((s) => !s); requestAnimationFrame(() => input.current?.focus()) }
}
addEventListener('keydown', h)
return () => removeEventListener('keydown', h)
}, [])
const key = (k: string) => (platform === 'win' ? WIN[k] ?? k : k)
const ql = q.toLowerCase()
const match = (it: { label: string; keys: string[] }) => !q || it.label.toLowerCase().includes(ql) || it.keys.some((k) => key(k).toLowerCase() === ql || (WORD[k] ?? '').toLowerCase().startsWith(ql))
const hl = (s: string) => { const i = s.toLowerCase().indexOf(ql); return !q || i < 0 ? s : <>{s.slice(0, i)}<mark>{s.slice(i, i + q.length)}</mark>{s.slice(i + q.length)}</> }
const filtered = groups.map((g) => ({ ...g, items: g.items.filter(match) })).filter((g) => g.items.length)
return (
<section className={['sd-kbs', className].filter(Boolean).join(' ')} data-fill={fill} aria-label="Keyboard shortcuts" hidden={!shown}>
<div className="sd-kbs__top">
<svg viewBox="0 0 16 16" aria-hidden><circle cx="7" cy="7" r="4.5" /><path d="m10.5 10.5 3 3" /></svg>
<input ref={input} aria-label="Search shortcuts" placeholder="Search shortcuts" value={q} onChange={(e) => setQ(e.target.value)} />
</div>
<div className="sd-kbs__body">
{filtered.map((g) => (
<div key={g.title} className="sd-kbs__g">
<div className="sd-kbs__gt">{g.title}</div>
{g.items.map((it) => (
<div key={it.label} className="sd-kbs__r">
<span>{hl(it.label)}</span>
<span className="sd-kbs__keys" aria-label={it.keys.map((k) => WORD[k] ?? key(k)).join(' ')}>{it.keys.map((k, i) => <kbd key={i} className="sd-kbs__k" aria-hidden>{key(k)}</kbd>)}</span>
</div>
))}
</div>
))}
{!filtered.length && <div className="sd-kbs__none">No shortcuts match “{q}”</div>}
</div>
<div className="sd-kbs__hint">Press <kbd className="sd-kbs__k">?</kbd> to toggle this sheet</div>
</section>
)
}.sd-kbs { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-kbs[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-kbs button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-kbs :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-kbs .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-kbs { width: min(100%, 330px); border-radius: 20px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); overflow: hidden; }
.sd-kbs__top { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--line); }
.sd-kbs__top svg { width: 15px; height: 15px; flex: none; fill: none; stroke: var(--mute); stroke-width: 1.6; stroke-linecap: round; }
.sd-kbs__top input { flex: 1; min-width: 0; height: 30px; border: 0; outline: 0; background: none; color: var(--fg); font: inherit; font-size: 14px; }
.sd-kbs__top input::placeholder { color: var(--mute); }
.sd-kbs__body { max-height: 340px; overflow: auto; padding: 6px 12px 12px; }
.sd-kbs__g { margin-top: 10px; animation: sd-kbs-in 200ms ease; }
@keyframes sd-kbs-in { from { opacity: 0; } }
.sd-kbs__gt { font-size: 11px; color: var(--mute); text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 4px; }
.sd-kbs__r { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 34px; font-size: 13.5px; border-bottom: 1px solid var(--line); }
.sd-kbs__r:last-child { border-bottom: 0; }
.sd-kbs__r mark { background: none; color: var(--fg); font-weight: 650; text-decoration: underline; text-underline-offset: 3px; }
.sd-kbs__keys { display: flex; gap: 3px; flex: none; }
.sd-kbs__k { display: inline-grid; place-items: center; min-width: 22px; height: 22px; padding: 0 5px; border-radius: 6px; background: var(--soft); box-shadow: inset 0 -1px 0 var(--line), inset 0 0 0 1px var(--line); font: 600 11px/1 ui-sans-serif, system-ui; }
.sd-kbs__none { padding: 24px 0; text-align: center; font-size: 13px; color: var(--mute); }
.sd-kbs__hint { padding: 8px 12px; border-top: 1px solid var(--line); font-size: 11.5px; color: var(--mute); display: flex; gap: 6px; align-items: center; }
@media (prefers-reduced-motion: reduce) { .sd-kbs *, .sd-kbs *::before, .sd-kbs *::after { animation: none !important; transition: none !important; } }Overview
Groups like General, Navigation and Editing list each command with its keys drawn as caps. The search matches command names and key names, and empty groups hide.
Press ? anywhere to toggle it. On Windows the ⌘ caps read Ctrl.
When to use
- Power-user apps: editors, email, project tools.
When not to use
- Apps with only a couple of shortcuts: list them in tooltips.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/shortcuts-sheet.json$
npx shadcn@latest add https://sundaeui.dev/r/shortcuts-sheet.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/shortcuts-sheet.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/shortcuts-sheet.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy shortcuts-sheet.tsx and shortcuts-sheet.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { ShortcutsSheet } from '@/components/sundae/shortcuts-sheet'
<ShortcutsSheet />Props
| Prop | Type | Default |
|---|---|---|
groups*Shortcut groups. | { title: string; items: { label: string; keys: string[] }[] }[] | — |
platformKey labels. | 'mac' | 'win' | 'mac' |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- A labelled region with a search input; each row exposes keys as text (“Command K”).
- The ? shortcut is ignored while typing in a field.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-kbs; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
Keyboard
- ?
- Toggle the sheet
FAQ
- Is Keyboard Shortcuts Sheet free?
- Yes. Keyboard Shortcuts Sheet is free for personal and commercial projects, with full source on this page.
- How do I install Keyboard Shortcuts Sheet?
- Run npx shadcn@latest add https://sundaeui.dev/r/shortcuts-sheet.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Keyboard Shortcuts Sheet support keyboard and screen readers?
- A labelled region with a search input; each row exposes keys as text (“Command K”). The ? shortcut is ignored while typing in a field.
- Does Keyboard Shortcuts Sheet respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All components →Info Tip
Components · RefinedEmoji Picker
Components · RefinedFAQ Search
Components · RefinedIcon Picker
Components · RefinedToast Stack
Components · Refined- Pro
Sheet Drawer
Components · Refined