Keyboard Shortcuts Help

Displays available keyboard shortcuts to the user.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/keyboard-shortcuts-help.json

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

2 stories available:

Code

"use client"; import { memo, useEffect, useRef } from "react"; import type { ReactNode } from "react"; import type { HeadingTag } from "../../lib/types"; import { cn } from "../../lib/utils"; export type KeyboardShortcut = { description: string; keys: readonly string[]; }; export type KeyboardShortcutsHelpProps = { /** Heading tag for the modal title. Defaults to `h2`. */ as?: HeadingTag; className?: string; closeIcon?: ReactNode; footer?: ReactNode; isOpen: boolean; onClose: () => void; shortcuts: readonly KeyboardShortcut[]; title?: string; }; // eslint-disable-next-line max-lines-per-function -- Modal with keyboard handling and focus trap function KeyboardShortcutsHelpImpl({ as: Heading = "h2", className, closeIcon, footer, isOpen, onClose, shortcuts, title = "Keyboard Shortcuts", }: KeyboardShortcutsHelpProps): React.ReactNode { const closeButtonRef = useRef<HTMLButtonElement>(null); // Focus trap and close on Escape useEffect(() => { if (!isOpen) return; closeButtonRef.current?.focus(); const handleKeyDown = (event: KeyboardEvent): void => { if (event.key === "Escape") { event.preventDefault(); onClose(); } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [isOpen, onClose]); // Prevent body scroll when open useEffect(() => { document.body.style.overflow = isOpen ? "hidden" : ""; return () => { document.body.style.overflow = ""; }; }, [isOpen]); if (!isOpen) return null; return ( <div aria-labelledby="shortcuts-title" aria-modal="true" className="fixed inset-0 z-50 flex items-center justify-center" role="dialog" > {/* Backdrop */} <div aria-hidden="true" className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} /> {/* Modal */} <div className={cn( "relative z-10 w-full max-w-sm rounded-lg bg-background p-6 shadow-xl mx-4", className, )} > {/* Header */} <div className="flex items-center justify-between mb-4"> <Heading className="text-lg font-semibold" id="shortcuts-title"> {title} </Heading> <button aria-label="Close shortcuts help" className="flex size-8 items-center justify-center rounded-md hover:bg-muted" onClick={onClose} ref={closeButtonRef} type="button" > {closeIcon ?? ( <svg className="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path d="M6 18L18 6M6 6l12 12" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} /> </svg> )} </button> </div> {/* Shortcuts List */} <div className="space-y-3"> {shortcuts.map((shortcut) => ( <div className="flex items-center justify-between text-sm" key={shortcut.description} > <span className="text-muted-foreground"> {shortcut.description} </span> <div className="flex gap-1"> {shortcut.keys.map((key) => ( <kbd className="inline-flex h-6 min-w-[24px] items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-xs" key={key} > {key} </kbd> ))} </div> </div> ))} </div> {/* Footer */} {footer ? ( <div className="mt-4 text-xs text-muted-foreground text-center"> {footer} </div> ) : null} </div> </div> ); } export const KeyboardShortcutsHelp = memo(KeyboardShortcutsHelpImpl); KeyboardShortcutsHelp.displayName = "KeyboardShortcutsHelp";

Dependencies

  • @vllnt/ui@^0.2.1