KeyboardShortcutsDialog
A dialog that lists every shortcut registered with useKeyboardShortcut, grouped and searchable. Mount it once near the app root, and bind v-model:open, because the dialog has no trigger of its own. Its rows come from the registry, so it takes no content props.
Default
<script setup lang="ts">
import { ref } from 'vue'
import { Button, KeyboardShortcutsDialog, useKeyboardShortcut } from 'frappe-ui'
const open = ref(false)
const readOnly = ref(false)
// Every registration lists itself in the dialog. `enabled: false` makes a
// shortcut inert and hides its row, so read-only mode drops the editing group.
// "Redo" is registered twice and renders as one row with two combos.
//
// This story runs inside the docs page, not in a frame. Every row that does
// nothing sets `preventDefault: false`, so the browser's own Save, Undo and
// Zoom shortcuts keep working for the reader.
useKeyboardShortcut([
{
combo: 'Mod+Shift+Slash',
description: 'Show keyboard shortcuts',
group: 'General',
allowInDialog: true,
handler: () => (open.value = !open.value),
},
{
combo: 'Mod+S',
description: 'Save',
group: 'General',
preventDefault: false,
handler: () => {},
},
{
combo: 'Mod+Z',
description: 'Undo',
group: 'Editing',
enabled: () => !readOnly.value,
preventDefault: false,
handler: () => {},
},
{
combo: 'Mod+Shift+Z',
description: 'Redo',
group: 'Editing',
enabled: () => !readOnly.value,
preventDefault: false,
handler: () => {},
},
{
combo: 'Mod+Y',
description: 'Redo',
group: 'Editing',
enabled: () => !readOnly.value,
preventDefault: false,
handler: () => {},
},
{
combo: 'Mod+Equal',
description: 'Zoom in',
group: 'View',
preventDefault: false,
handler: () => {},
},
{
combo: 'Mod+Minus',
description: 'Zoom out',
group: 'View',
preventDefault: false,
handler: () => {},
},
])
</script>
<template>
<div class="flex items-center gap-3">
<Button label="Open shortcuts" @click="open = true" />
<Button
:label="readOnly ? 'Leave read-only mode' : 'Enter read-only mode'"
@click="readOnly = !readOnly"
/>
<!-- A story has far fewer rows than the default `searchThreshold` of 20.
Lower it so the search field shows here. -->
<KeyboardShortcutsDialog v-model:open="open" :search-threshold="3" />
</div>
</template>Two rules shape what the dialog shows:
- A shortcut whose
enabledisfalseis inert and hidden. A shortcut the user cannot press is not advertised. Toggle read-only mode in the preview above to watch the Editing group leave. - Shortcuts that share a group and a description merge into one row. The first combo is the row's combo; the rest render after a
/as alternatives. Undo and Redo above are three registrations and two rows.
The dialog reads enabled each time it opens, and again whenever a shortcut registers or unregisters. A getter that reads untracked state, such as document.activeElement, is correct on every open.
Above searchThreshold rows a search field appears. It matches the text a row draws as well as the name behind it, so a row for Mod+Slash answers to both / and slash. The query clears when the dialog closes. The preview above lowers searchThreshold to 3, because it has too few rows to pass the default of 20.
Open it from a shortcut
<script setup lang="ts">
import { KeyboardShortcutsDialog, useKeyboardShortcut } from 'frappe-ui'
import { ref } from 'vue'
const open = ref(false)
useKeyboardShortcut({
combo: 'Mod+Shift+Slash',
description: 'Show keyboard shortcuts',
group: 'General',
allowInDialog: true,
handler: () => (open.value = !open.value),
})
</script>
<template>
<KeyboardShortcutsDialog v-model:open="open" />
</template>allowInDialog: true lets the combo answer while the dialog holds focus, so the same keys close it again. The handler toggles for that reason.
Mod+Shift+Slash is the ? most apps use. The combo names the physical key, not the character it types. See the combo grammar.
A custom help surface
<script setup lang="ts">
import { ref } from 'vue'
import {
Button,
KeyboardShortcut,
KeyboardShortcutsDialog,
useKeyboardShortcut,
} from 'frappe-ui'
const open = ref(false)
// The default slot receives the grouped, enabled shortcuts, so an app can
// render its own help surface without reading the registry itself. Both rows
// have an empty handler, so both set `preventDefault: false` and leave the
// docs page they run in alone.
//
// Alt pairs with a digit here, not a letter: macOS rewrites `event.key` while
// Option is held, so an `Alt+<letter>` combo can miss there.
useKeyboardShortcut([
{
combo: 'Mod+Alt+Digit1',
description: 'New page',
group: 'Pages',
preventDefault: false,
handler: () => {},
},
{
combo: 'Mod+Alt+Digit2',
description: 'Duplicate page',
group: 'Pages',
preventDefault: false,
handler: () => {},
},
])
</script>
<template>
<Button label="Open shortcuts" @click="open = true" />
<KeyboardShortcutsDialog v-model:open="open" v-slot="{ groups }">
<div class="space-y-4">
<section v-for="group in groups" :key="group.name">
<h4 class="mb-2 text-sm-medium text-ink-gray-5">{{ group.name }}</h4>
<div
v-for="shortcut in group.shortcuts"
:key="shortcut.description"
class="flex items-center justify-between py-1"
>
<span class="text-p-base text-ink-gray-7">
{{ shortcut.description }}
</span>
<KeyboardShortcut
:combo="shortcut.combo"
:alt-combos="shortcut.altCombos"
bg
/>
</div>
</section>
</div>
</KeyboardShortcutsDialog>
</template>The default slot receives the grouped shortcuts, so an app can render its own layout. The library exports no registry reader; this slot is the way in.
Styling hooks
Every part carries a data-slot (P10). Target them from CSS instead of reaching for a class prop:
data-slot | Element |
|---|---|
header | title row |
title | dialog title |
search | the search field, when shown |
empty | the empty message; data-state is empty or no-results |
groups | the grid of groups |
group | one group column |
group-title | a group heading |
shortcut | one row |
description | the row label |
shortcut-keys | the row's KeyboardShortcut |
API Reference
Show types
export interface KeyboardShortcutsDialogProps {
/** Dialog title (default: "Keyboard Shortcuts"). */
title?: string
/**
* Top padding while the dialog sits at the top of the screen. Takes the
* same values as `Dialog`'s own prop (default: "5vh").
*/
paddingTop?: string | number
/**
* The search input appears once the number of rows passes this count.
* Rows, not registrations: shortcuts that merge count once (default: 20).
*/
searchThreshold?: number
}Dialog title (default: "Keyboard Shortcuts").
Top padding while the dialog sits at the top of the screen. Takes the same values as `Dialog`'s own prop (default: "5vh").
The search input appears once the number of rows passes this count. Rows, not registrations: shortcuts that merge count once (default: 20).
| Slot | Payload |
|---|---|
default | { groups: KeyboardShortcutGroup[]; } Replaces the default grid. Receives the grouped, enabled shortcuts the search leaves. |
Replaces the default grid. Receives the grouped, enabled shortcuts the search leaves.
| Event | Payload |
|---|---|
update:open | [value: boolean] Fired when the open state changes. |
Fired when the open state changes.