Frappe UIFrappe UI

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

vue
<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 enabled is false is 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

vue
<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

vue
<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-slotElement
headertitle row
titledialog title
searchthe search field, when shown
emptythe empty message; data-state is empty or no-results
groupsthe grid of groups
groupone group column
group-titlea group heading
shortcutone row
descriptionthe row label
shortcut-keysthe row's KeyboardShortcut

API Reference

Show types
typescript
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
}
title
= "Keyboard Shortcuts"
string

Dialog title (default: "Keyboard Shortcuts").

paddingTop
= "5vh"
string | number

Top padding while the dialog sits at the top of the screen. Takes the same values as `Dialog`'s own prop (default: "5vh").

searchThreshold
= 20
number

The search input appears once the number of rows passes this count. Rows, not registrations: shortcuts that merge count once (default: 20).

open
= false
boolean
default
{ groups: KeyboardShortcutGroup[]; }

Replaces the default grid. Receives the grouped, enabled shortcuts the search leaves.

update:open
[value: boolean]

Fired when the open state changes.