defuss-shadcn / Guides / global-keys
Global Key Commands
Page-wide shortcuts - ⌘B for the sidebar, arrow keys for a deck, your own ? for help - all run through one shared keydown listener in the runtime: df$.shadcn.shared.bindGlobalKeys. By default it never fires while someone is typing in a field, so a shortcut can't eat a letter meant for a form - and a handler that needs keys there too opts in with { editable: true }.
On this page (5)
§The shared listener
bindGlobalKeys(handler) adds a handler to a single document-level keydown listener that core.js installs on the first registration and removes again after the last unbind. Instead of every component hand-rolling its own listener and guard, they all share this one, which means:
| Rule | What it means |
|---|---|
| Typing is never a shortcut (by default) | Keys aimed at an input, textarea, select or contenteditable element are not dispatched - unless the handler was bound with the editable: true option; it then decides itself (isEditableTarget(event)) |
| Registration order | Handlers run in the order they were bound |
| return true stops the rest | A handler that took the key returns true - later handlers skip that event |
| Unbind | bindGlobalKeys returns its own unbind function; the last unbind removes the document listener |
| Safe before a document exists | Binding outside a browser is a no-op that still returns a working unbind |
const { bindGlobalKeys } = df$.shadcn.shared; // installed by core.js / all.jsconst unbind = bindGlobalKeys((event) => { if (event.key !== '?') return; // not ours - let later handlers see it event.preventDefault(); df$('#help').get(0).showModal(); // df$ selects; showModal() stays native return true; // ours - stop here});// later, e.g. when the view that owns the shortcut goes awayunbind();§Your own shortcuts
By default the shared listener is typing-safe: a key pressed while an <input>, <textarea>, <select> or contenteditable element has focus is never handed to any handler. That is deliberate - otherwise a ? or / typed into a search box would open help or jump focus instead of being typed. The catch: with the default, ? and / simply do nothing while the field has focus.
When a shortcut should work inside a field, the handler opts in per registration with { editable: true } - and then owns the decision itself: df$.shadcn.shared.isEditableTarget(event) tells it the key came from a field, and it lets ordinary typing through. The example below does exactly that:
- outside the field: ? opens help, / focuses the search
- in the empty field: ? still opens help (a lone ? is not a search), / is ignored (you are already there)
- in a field with text: both are just characters - the handler returns early and the browser types them
- Esc in the field leaves it (blur), so ? / / work again right away
const { bindGlobalKeys, isEditableTarget } = df$.shadcn.shared;bindGlobalKeys((event) => { const inField = isEditableTarget(event); if (inField && event.target.value !== '' && event.key !== 'Escape') return; // typing - hands off if (event.key === '?') { event.preventDefault(); openHelp(); return true; } if (event.key === '/' && !inField) { event.preventDefault(); search.focus(); return true; } if (event.key === 'Escape' && inField) { event.target.blur(); return true; }}, { editable: true }); // ← without this, keys typed into fields never arrive here§Help on ?, search on /
One bindGlobalKeys handler registered with { editable: true }: ? opens help everywhere except inside a field that already holds text, / jumps into the search, Esc leaves the field. Click into the example first - keyboard focus has to be inside it.
§Shortcuts the components ship
| Component | Keys | Listener |
|---|---|---|
| Sidebar | ⌘B / Ctrl+B - dock / undock the first .app-sidebar | shared (bindGlobalKeys) |
| Presentation | → PageDown Space next · ← PageUp previous · Home End first / last · N notes · F fullscreen | shared (bindGlobalKeys) |
| Animation Canvas | ← → ↑ ↓ to the neighbouring slide · O / Esc overview | shared (bindGlobalKeys) |
| Command | ⌘K / Ctrl+K - open / close the command palette | its own document listener - on purpose: ⌘K must work while typing in a field |
Components only take a key when it is theirs - the sidebar only claims ⌘B when the page has a sidebar, a deck only claims arrow keys when it has a slide to go to - so the browser's own shortcuts keep working everywhere else.
§Good shortcuts
- Return
trueonly when you actually handled the key, and callpreventDefault()then - never for keys you ignore. - Prefer modifier shortcuts (⌘/Ctrl+key) for page-wide actions. A single-character shortcut (?, /, N) must be possible to turn off or remap, or only be active while its widget has focus (WCAG 2.1.4, Character Key Shortcuts) - speech-input users otherwise trigger it by accident.
- List your shortcuts somewhere visible - a help dialog on ? is the common convention.
- Unbind when the view that owns the shortcut goes away (an SPA route change, a closed panel).
Comments, ideas or improvements? Edit this page's source on GitHub