Keyboard shortcut: Ctrl + K
Get started

Hotkeys

A keyboard shortcut, displayed. It renders as nested <kbd> elements, spells its glyphs per platform, and can listen for the combination it shows if you ask it to.

Usage

vue
Keyboard shortcut: Ctrl + K
<script setup lang="ts">
import { VHotkeys } from 'vectis-ui'
</script>

<template>
  <VHotkeys keys="mod+k" />
</template>

Examples

What you can write

keys is a plain string, +-separated, where neither case nor spaces matter. mod is the modifier that belongs to the system, Command on a Mac and Ctrl everywhere else, where meta names that physical key literally. A token the design system does not know is drawn exactly as it was written, and the + key is written plus.

You writemacOSWindows and Linux
modKeyboard shortcut: CommandKeyboard shortcut: Ctrl
metacmdcommandwinsuperKeyboard shortcut: CommandKeyboard shortcut: Win
ctrlcontrolKeyboard shortcut: CtrlKeyboard shortcut: Ctrl
altoptionoptKeyboard shortcut: AltKeyboard shortcut: Alt
shiftKeyboard shortcut: ShiftKeyboard shortcut: Shift
enterreturnKeyboard shortcut: EnterKeyboard shortcut: Enter
escescapeKeyboard shortcut: EscKeyboard shortcut: Esc
spaceKeyboard shortcut: SpaceKeyboard shortcut: Space
backspaceKeyboard shortcut: BackspaceKeyboard shortcut: Backspace
deletedelKeyboard shortcut: DelKeyboard shortcut: Del
tabKeyboard shortcut: TabKeyboard shortcut: Tab
updownleftrightKeyboard shortcut: Up arrowKeyboard shortcut: Down arrowKeyboard shortcut: Left arrowKeyboard shortcut: Right arrowKeyboard shortcut: Up arrowKeyboard shortcut: Down arrowKeyboard shortcut: Left arrowKeyboard shortcut: Right arrow
plusKeyboard shortcut: +Keyboard shortcut: +
kf5,Keyboard shortcut: KKeyboard shortcut: F5Keyboard shortcut: ,Keyboard shortcut: KKeyboard shortcut: F5Keyboard shortcut: ,

Variants

variant draws the caps tinted, outlined or raised. There is no tone and no colour prop: every paint derives from the colour the component inherits.

vue
Keyboard shortcut: Ctrl + Shift + P

soft

Keyboard shortcut: Ctrl + Shift + P

outline

Keyboard shortcut: Ctrl + Shift + P

elevated

<script setup lang="ts">
import { VHotkeys, VTypography, type HotkeysVariant } from 'vectis-ui'

const VARIANTS: HotkeysVariant[] = ['soft', 'outline', 'elevated']
</script>

<template>
  <div class="row">
    <div v-for="variant in VARIANTS" :key="variant" class="cell">
      <VHotkeys keys="mod+shift+p" :variant="variant" />
      <VTypography variant="caption" tone="muted">{{ variant }}</VTypography>
    </div>
  </div>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-6);
}
.cell {
  display: grid;
  justify-items: center;
  gap: var(--vectis-space-2);
}
</style>

Sizes

size takes xs or sm, and compact takes 4px off either of them. A cap holding a single character is square.

vue
Keyboard shortcut: Ctrl + K

xs

Keyboard shortcut: Ctrl + K

sm

Keyboard shortcut: Ctrl + K

xs compact

Keyboard shortcut: Ctrl + K

sm compact

<script setup lang="ts">
import { VHotkeys, VTypography, type HotkeysSize } from 'vectis-ui'

/* Two sizes only. A shortcut is chrome beside other text, so `xs` is the default and
   `sm` is already as large as one usefully gets. */
const SIZES: HotkeysSize[] = ['xs', 'sm']
</script>

<template>
  <div class="row">
    <div v-for="size in SIZES" :key="size" class="cell">
      <VHotkeys keys="mod+k" :size="size" variant="outline" />
      <VTypography variant="caption" tone="muted">{{ size }}</VTypography>
    </div>

    <div v-for="size in SIZES" :key="`${size}-compact`" class="cell">
      <VHotkeys keys="mod+k" :size="size" compact variant="outline" />
      <VTypography variant="caption" tone="muted">{{ size }} compact</VTypography>
    </div>
  </div>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--vectis-space-6);
}
.cell {
  display: grid;
  justify-items: center;
  gap: var(--vectis-space-2);
}
</style>

Attached

attached moves the decoration from each cap to the shortcut as a whole, so the combination reads as one key. It is purely visual: the markup, the caps and the announced name are identical either way.

vue
Keyboard shortcut: Ctrl + Shift + K

One cap per key

Keyboard shortcut: Ctrl + Shift + K

attached

Keyboard shortcut: Ctrl + Shift + K

attached, no separator

<script setup lang="ts">
import { VHotkeys, VTypography } from 'vectis-ui'
</script>

<template>
  <div class="row">
    <div class="cell">
      <VHotkeys keys="mod+shift+k" variant="outline" />
      <VTypography variant="caption" tone="muted">One cap per key</VTypography>
    </div>

    <!-- The decoration moves from each cap to the shortcut as a whole, which is what
         puts the separator inside the key instead of between two of them. -->
    <div class="cell">
      <VHotkeys keys="mod+shift+k" attached variant="outline" />
      <VTypography variant="caption" tone="muted">attached</VTypography>
    </div>

    <div class="cell">
      <VHotkeys keys="mod+shift+k" attached separator="" variant="outline" />
      <VTypography variant="caption" tone="muted">attached, no separator</VTypography>
    </div>
  </div>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-6);
}
.cell {
  display: grid;
  justify-items: center;
  gap: var(--vectis-space-2);
}
</style>

Platform

platform forces the system instead of reading it once the component is in the page, which is what a table showing every system needs.

vue

macOS

Keyboard shortcut: Command + KKeyboard shortcut: Command + Shift + EnterKeyboard shortcut: Alt + Backspace

Windows

Keyboard shortcut: Ctrl + KKeyboard shortcut: Win + Shift + EnterKeyboard shortcut: Alt + Backspace

Linux

Keyboard shortcut: Ctrl + KKeyboard shortcut: Super + Shift + EnterKeyboard shortcut: Alt + Backspace

Unknown

Keyboard shortcut: Ctrl + KKeyboard shortcut: Super + Shift + EnterKeyboard shortcut: Alt + Backspace
<script setup lang="ts">
import { VHotkeys, VTypography, type HotkeysPlatform } from 'vectis-ui'

/* The four the component knows. Without the prop it detects the one the reader is on,
   which is what the shortcuts everywhere else on this page are doing. */
const PLATFORMS: { value: HotkeysPlatform; label: string }[] = [
  { value: 'mac', label: 'macOS' },
  { value: 'windows', label: 'Windows' },
  { value: 'linux', label: 'Linux' },
  { value: 'other', label: 'Unknown' },
]
</script>

<template>
  <div class="grid">
    <template v-for="platform in PLATFORMS" :key="platform.value">
      <VTypography variant="caption" tone="muted">{{ platform.label }}</VTypography>
      <div class="keys">
        <VHotkeys keys="mod+k" :platform="platform.value" variant="outline" />
        <VHotkeys keys="meta+shift+enter" :platform="platform.value" variant="outline" />
        <VHotkeys keys="alt+backspace" :platform="platform.value" variant="outline" />
      </div>
    </template>
  </div>
</template>

<style scoped>
.grid {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--vectis-space-3) var(--vectis-space-5);
}
.keys {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-3);
}
</style>

Separator

separator is what is written between two caps, + by default. An empty string leaves the gap in place and gives the macOS convention.

vue
Keyboard shortcut: Ctrl + Shift + K

The default

Keyboard shortcut: Ctrl + Shift + K

Nothing, the macOS convention

Keyboard shortcut: Ctrl + Shift + K

A middle dot

Keyboard shortcut: Ctrl + Shift + K

A slash

<script setup lang="ts">
import { VHotkeys, VTypography } from 'vectis-ui'

/* The caps are laid out with a gap of their own, so the separator is what is WRITTEN
   between them and never the space around it: an empty string leaves the gap alone
   and gives the macOS convention, where the symbols simply follow one another. */
const SEPARATORS = [
  { value: '+', label: 'The default' },
  { value: '', label: 'Nothing, the macOS convention' },
  { value: '·', label: 'A middle dot' },
  { value: '/', label: 'A slash' },
]
</script>

<template>
  <div class="grid">
    <template v-for="separator in SEPARATORS" :key="separator.label">
      <VHotkeys keys="mod+shift+k" :separator="separator.value" variant="outline" />
      <VTypography variant="caption" tone="muted">{{ separator.label }}</VTypography>
    </template>
  </div>
</template>

<style scoped>
.grid {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--vectis-space-3) var(--vectis-space-5);
  justify-items: start;
}
</style>

In text and in components

A cap takes its size from the size scale, xs by default, and sits on the middle of the line around it. Its usual homes are the end of a menu row, a tooltip, and the control the shortcut is a second route to.

vue

Press Keyboard shortcut: Ctrl + K to open the command palette, then type the first letters of what you are after. Keyboard shortcut: Esc puts it away again.

<script setup lang="ts">
import {
  VButton,
  VHotkeys,
  VMenu,
  VMenuItem,
  VMenuSeparator,
  VTooltip,
  VTypography,
} from 'vectis-ui'

const COMMANDS = [
  { label: 'New file', keys: 'mod+n' },
  { label: 'Save', keys: 'mod+s' },
  { label: 'Save as', keys: 'mod+shift+s' },
]
</script>

<template>
  <div class="stack">
    <!-- The cap takes its size from the text it sits in, so a shortcut written into a
         sentence keeps the line it is on. -->
    <VTypography class="prose">
      Press <VHotkeys keys="mod+k" /> to open the command palette, then type the first letters of
      what you are after. <VHotkeys keys="esc" /> puts it away again.
    </VTypography>

    <!-- At the end of a command row, which is where a shortcut is usually read. -->
    <VMenu>
      <template #trigger="{ triggerProps }">
        <VButton v-bind="triggerProps" variant="outline" tone="neutral">File</VButton>
      </template>
      <VMenuItem v-for="command in COMMANDS" :key="command.keys" :label="command.label">
        <template #end><VHotkeys :keys="command.keys" /></template>
      </VMenuItem>
      <VMenuSeparator />
      <VMenuItem label="Settings">
        <template #end><VHotkeys keys="mod+," /></template>
      </VMenuItem>
    </VMenu>

    <!-- Inside a tooltip, which is what its `#content` slot is for: rich but never
         interactive. -->
    <VTooltip>
      <template #default="{ triggerProps }">
        <VButton v-bind="triggerProps" variant="outline" tone="neutral">Save</VButton>
      </template>
      <template #content>Save this file <VHotkeys keys="mod+s" size="xs" /></template>
    </VTooltip>

    <!-- And inside the control the shortcut is a second route to. -->
    <VButton variant="outline" tone="neutral" class="search">
      Search the docs
      <VHotkeys keys="mod+k" variant="outline" />
    </VButton>
  </div>
</template>

<style scoped>
.stack {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--vectis-space-5);
}
.prose {
  max-inline-size: 34rem;
  line-height: 1.9;
}
.search {
  inline-size: 18rem;
  justify-content: space-between;
}
</style>

Listening

listen makes the component watch for the combination it shows and emit trigger. Modifiers are matched exactly, so mod+k and mod+shift+k can both exist. allowDefault keeps the browser binding, and allowInInput lets the shortcut fire while the reader is typing in a field. Escape is reported but never cancelled, so it still closes dialogs. Matching reads the character the key produced: a symbol typed with Shift (?), a digit on an AZERTY layout and an Option + letter on macOS do not match, so prefer letters and named keys.

vue

Keyboard shortcut: Ctrl + KPress it anywhere on the page. Fired 0 times.

Keyboard shortcut: Ctrl + S With allowDefault, the browser still saves the page. Fired 0 times.

Keyboard shortcut: Ctrl + JQuiet while a field has the focus: 0

Keyboard shortcut: Ctrl + JWith allowInInput, it fires there too: 0

<script setup lang="ts">
import { ref } from 'vue'
import { VHotkeys, VInput, VTypography } from 'vectis-ui'

const opened = ref(0)
const saved = ref(0)
const strict = ref(0)
const permissive = ref(0)
</script>

<template>
  <div class="stack">
    <!-- No platform is pinned, so the caps show what this keyboard has and the matcher
         answers to the same thing: a Mac reader presses Command, everyone else Ctrl. -->
    <p class="line">
      <VHotkeys keys="mod+k" variant="outline" listen @trigger="opened++" />
      <VTypography as="span">Press it anywhere on the page. Fired {{ opened }} times.</VTypography>
    </p>

    <!-- The browser's own binding is cancelled unless allowDefault says otherwise,
         which is the whole point of taking over a combination it already uses. Here
         both happen: the count goes up and the browser opens its save dialog. -->
    <p class="line">
      <VHotkeys keys="mod+s" variant="outline" listen allow-default @trigger="saved++" />
      <VTypography as="span" tone="muted">
        With allowDefault, the browser still saves the page. Fired {{ saved }} times.
      </VTypography>
    </p>

    <VInput label="Type in here, then press the shortcut below" class="field" />

    <p class="line">
      <VHotkeys keys="mod+j" variant="outline" listen @trigger="strict++" />
      <VTypography as="span">Quiet while a field has the focus: {{ strict }}</VTypography>
    </p>

    <p class="line">
      <VHotkeys keys="mod+j" variant="outline" listen allow-in-input @trigger="permissive++" />
      <VTypography as="span">With allowInInput, it fires there too: {{ permissive }}</VTypography>
    </p>
  </div>
</template>

<style scoped>
.stack {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--vectis-space-4);
}
.line {
  display: flex;
  align-items: center;
  gap: var(--vectis-space-3);
  margin: 0;
}
.field {
  inline-size: 22rem;
}
</style>

API

Props

PropTypeDefault
keysstringnone
The combination, separated by +: mod+k, ctrl+shift+p, alt+enter. Case and spaces do not matter. mod is the cross-platform modifier, Command on macOS and Ctrl everywhere else, while meta is the literal Command or Windows key. An unknown token is displayed as it was declared, and the + key itself is written plus.
variantHotkeysVariant'soft' | 'outline' | 'elevated''soft'
How a key cap is drawn: tinted, outlined, or raised off the page.
attachedbooleanfalse
Draws the whole combination as a single key rather than as several, which puts the separator inside the key instead of between two of them. It is purely visual: the markup and the announced name are identical either way.
sizeHotkeysSize'xs' | 'sm''xs'
The size of the caps. A shortcut is chrome beside other text, so it starts at the smallest.
compactbooleanfalse
Takes 4px off the height, leaving the padding and the text as they are.
platformHotkeysPlatform'mac' | 'windows' | 'linux' | 'other'none
Forces the keyboard's operating system instead of detecting it, for a deterministic rendering or a host that already knows.
separatorstring'+'
What is written between two caps. An empty string gives the macOS convention, where the symbols simply follow one another.
listenbooleanfalse
Actually listens for the combination and reports it. It is off by default: a component whose job is to display a shortcut must not capture the page's keyboard without being asked.
allowDefaultbooleanfalse
While listening, lets the browser go on doing whatever the combination normally does. Left out, the browser is stopped, which is the entire point of taking a combination over. Escape is never stopped: it has to stay the close request of dialogs.
allowInInputbooleanfalse
While listening, fires even when the reader is typing in a field. It is off by default, so a shortcut cannot fire in the middle of a sentence.
labelstringnone
What screen readers announce. It falls back to the design system dictionary, which spells the modifiers as words: the glyph wins on screen, the word wins in the accessible name.

Events

EventType
trigger[event: KeyboardEvent]
The combination was pressed, with the original keyboard event. It only fires while listen is set.