<script setup lang="ts">
import { TanStackDevtools } from '@tanstack/vue-devtools'
import {
HotkeysProvider,
formatForDisplay,
useHotkey,
} from '@tanstack/vue-hotkeys'
import { HotkeysDevtoolsPanel } from '@tanstack/vue-hotkeys-devtools'
import type { Hotkey } from '@tanstack/vue-hotkeys'
import { nextTick, ref, watch } from 'vue'
const lastHotkey = ref<Hotkey | null>(null)
const saveCount = ref(0)
const incrementCount = ref(0)
const enabled = ref(true)
const activeTab = ref(1)
const navigationCount = ref(0)
const functionKeyCount = ref(0)
const multiModifierCount = ref(0)
const editingKeyCount = ref(0)
const modalOpen = ref(false)
const editorContent = ref('')
const sidebarShortcutCount = ref(0)
const modalShortcutCount = ref(0)
const editorShortcutCount = ref(0)
const sidebarRef = ref<HTMLDivElement | null>(null)
const modalRef = ref<HTMLDivElement | null>(null)
const editorRef = ref<HTMLTextAreaElement | null>(null)
const plugins = [{ name: 'TanStack Hotkeys', component: HotkeysDevtoolsPanel }]
useHotkey('Mod+S', (_event, { hotkey, parsedHotkey }) => {
lastHotkey.value = hotkey
saveCount.value++
console.log('Hotkey triggered:', hotkey)
console.log('Parsed hotkey:', parsedHotkey)
})
useHotkey(
'Mod+K',
(_event, { hotkey }) => {
lastHotkey.value = hotkey
incrementCount.value++
},
{ requireReset: true },
)
useHotkey(
'Mod+E',
(_event, { hotkey }) => {
lastHotkey.value = hotkey
alert('This hotkey can be toggled!')
},
{ enabled },
)
useHotkey('Mod+1', () => {
lastHotkey.value = 'Mod+1'
activeTab.value = 1
})
useHotkey('Mod+2', () => {
lastHotkey.value = 'Mod+2'
activeTab.value = 2
})
useHotkey('Mod+3', () => {
lastHotkey.value = 'Mod+3'
activeTab.value = 3
})
useHotkey('Mod+4', () => {
lastHotkey.value = 'Mod+4'
activeTab.value = 4
})
useHotkey('Mod+5', () => {
lastHotkey.value = 'Mod+5'
activeTab.value = 5
})
useHotkey('Shift+ArrowUp', () => {
lastHotkey.value = 'Shift+ArrowUp'
navigationCount.value++
})
useHotkey('Shift+ArrowDown', () => {
lastHotkey.value = 'Shift+ArrowDown'
navigationCount.value++
})
useHotkey('Alt+ArrowLeft', () => {
lastHotkey.value = 'Alt+ArrowLeft'
navigationCount.value++
})
useHotkey('Alt+ArrowRight', () => {
lastHotkey.value = 'Alt+ArrowRight'
navigationCount.value++
})
useHotkey('Mod+Home', () => {
lastHotkey.value = 'Mod+Home'
navigationCount.value++
})
useHotkey('Mod+End', () => {
lastHotkey.value = 'Mod+End'
navigationCount.value++
})
useHotkey('Control+PageUp', () => {
lastHotkey.value = 'Control+PageUp'
navigationCount.value++
})
useHotkey('Control+PageDown', () => {
lastHotkey.value = 'Control+PageDown'
navigationCount.value++
})
useHotkey('Meta+F4', () => {
lastHotkey.value = 'Alt+F4'
functionKeyCount.value++
alert('Alt+F4 pressed (normally closes window)')
})
useHotkey('Control+F5', () => {
lastHotkey.value = 'Control+F5'
functionKeyCount.value++
})
useHotkey('Mod+F1', () => {
lastHotkey.value = 'Mod+F1'
functionKeyCount.value++
})
useHotkey('Shift+F10', () => {
lastHotkey.value = 'Shift+F10'
functionKeyCount.value++
})
useHotkey('Mod+Shift+S', () => {
lastHotkey.value = 'Mod+Shift+S'
multiModifierCount.value++
})
useHotkey('Mod+Shift+Z', () => {
lastHotkey.value = 'Mod+Shift+Z'
multiModifierCount.value++
})
useHotkey({ key: 'A', ctrl: true, alt: true }, () => {
lastHotkey.value = 'Control+Alt+A'
multiModifierCount.value++
})
useHotkey('Control+Shift+N', () => {
lastHotkey.value = 'Control+Shift+N'
multiModifierCount.value++
})
useHotkey('Mod+Alt+T', () => {
lastHotkey.value = 'Mod+Alt+T'
multiModifierCount.value++
})
useHotkey('Control+Alt+Shift+X', () => {
lastHotkey.value = 'Control+Alt+Shift+X'
multiModifierCount.value++
})
useHotkey('Mod+Enter', () => {
lastHotkey.value = 'Mod+Enter'
editingKeyCount.value++
})
useHotkey('Shift+Enter', () => {
lastHotkey.value = 'Shift+Enter'
editingKeyCount.value++
})
useHotkey('Mod+Backspace', () => {
lastHotkey.value = 'Mod+Backspace'
editingKeyCount.value++
})
useHotkey('Mod+Delete', () => {
lastHotkey.value = 'Mod+Delete'
editingKeyCount.value++
})
useHotkey('Control+Tab', () => {
lastHotkey.value = 'Control+Tab'
editingKeyCount.value++
})
useHotkey('Shift+Tab', () => {
lastHotkey.value = 'Shift+Tab'
editingKeyCount.value++
})
useHotkey('Mod+Space', () => {
lastHotkey.value = 'Mod+Space'
editingKeyCount.value++
})
useHotkey({ key: 'Escape' }, () => {
lastHotkey.value = null
saveCount.value = 0
incrementCount.value = 0
navigationCount.value = 0
functionKeyCount.value = 0
multiModifierCount.value = 0
editingKeyCount.value = 0
activeTab.value = 1
})
useHotkey('F12', () => {
lastHotkey.value = 'F12'
functionKeyCount.value++
})
watch(modalOpen, async (isOpen) => {
if (isOpen) {
await nextTick()
modalRef.value?.focus()
}
})
useHotkey(
'Mod+B',
() => {
lastHotkey.value = 'Mod+B'
sidebarShortcutCount.value++
alert(
'Sidebar shortcut triggered! This only works when the sidebar area is focused.',
)
},
{ target: sidebarRef },
)
useHotkey(
'Mod+N',
() => {
lastHotkey.value = 'Mod+N'
sidebarShortcutCount.value++
},
{ target: sidebarRef },
)
useHotkey(
'Escape',
() => {
lastHotkey.value = 'Escape'
modalShortcutCount.value++
modalOpen.value = false
},
{ target: modalRef, enabled: modalOpen },
)
useHotkey(
'Mod+Enter',
() => {
lastHotkey.value = 'Mod+Enter'
modalShortcutCount.value++
alert('Modal submit shortcut!')
},
{ target: modalRef, enabled: modalOpen },
)
useHotkey(
'Mod+S',
() => {
lastHotkey.value = 'Mod+S'
editorShortcutCount.value++
alert(
`Editor content saved: "${editorContent.value.substring(0, 50)}${editorContent.value.length > 50 ? '...' : ''}"`,
)
},
{ target: editorRef },
)
useHotkey(
'Mod+/',
() => {
lastHotkey.value = 'Mod+/'
editorShortcutCount.value++
editorContent.value += '\n// Comment added via shortcut'
},
{ target: editorRef },
)
useHotkey(
'Mod+K',
() => {
lastHotkey.value = 'Mod+K'
editorShortcutCount.value++
editorContent.value = ''
},
{ target: editorRef },
)
const basicCode = `useHotkey('Mod+S', (_event, { hotkey, parsedHotkey }) => {
console.log('Hotkey:', hotkey)
console.log('Parsed:', parsedHotkey)
})`
const requireResetCode = `useHotkey(
'Mod+K',
(event, { hotkey }) => {
setCount(c => c + 1)
},
{ requireReset: true }
)`
const conditionalCode = `const [enabled, setEnabled] = useState(true)
useHotkey(
'Mod+E',
(event, { hotkey }) => {
alert('Triggered!')
},
{ enabled }
)`
const numberCode = `useHotkey('Mod+1', () => setActiveTab(1))
useHotkey('Mod+2', () => setActiveTab(2))`
const navigationCode = `useHotkey('Shift+ArrowUp', () => selectUp())
useHotkey('Alt+ArrowLeft', () => navigateBack())
useHotkey('Mod+Home', () => goToStart())
useHotkey('Control+PageUp', () => previousPage())`
const functionCode = `useHotkey('Alt+F4', () => closeWindow())
useHotkey('Control+F5', () => hardRefresh())
useHotkey('Mod+F1', () => showHelp())
useHotkey('F12', () => openDevTools())`
const multiModifierCode = `useHotkey('Mod+Shift+S', () => saveAs())
useHotkey('Mod+Shift+Z', () => redo())
useHotkey('Control+Alt+A', () => specialAction())
useHotkey('Control+Alt+Shift+X', () => complexAction())`
const editingCode = `useHotkey('Mod+Enter', () => submitForm())
useHotkey('Shift+Enter', () => insertNewline())
useHotkey('Mod+Backspace', () => deleteWord())
useHotkey('Control+Tab', () => nextTab())
useHotkey('Mod+Space', () => toggle())`
const scopedCode = `// Scoped to a ref
const sidebarRef = useRef<HTMLDivElement>(null)
useHotkey(
'Mod+B',
() => {
console.log('Sidebar shortcut!')
},
{ target: sidebarRef }
)
// Scoped to a modal (only when open)
const modalRef = useRef<HTMLDivElement>(null)
const [isOpen, setIsOpen] = useState(false)
useHotkey(
'Escape',
() => setIsOpen(false),
{ target: modalRef, enabled: isOpen }
)
// Scoped to an editor
const editorRef = useRef<HTMLTextAreaElement>(null)
useHotkey(
'Mod+S',
() => saveEditorContent(),
{ target: editorRef }
)`
</script>
<template>
<HotkeysProvider>
<div class="app">
<header>
<h1>useHotkey</h1>
<p>
Register keyboard shortcuts with callback context containing the
hotkey and parsed hotkey information.
</p>
</header>
<main>
<section class="demo-section">
<h2>Basic Hotkey</h2>
<p>
Press <kbd>{{ formatForDisplay('Mod+S') }}</kbd> to trigger
</p>
<div class="counter">Save triggered: {{ saveCount }}x</div>
<pre class="code-block">{{ basicCode }}</pre>
</section>
<section class="demo-section">
<h2>With requireReset</h2>
<p>
Hold <kbd>{{ formatForDisplay('Mod+K') }}</kbd> — only increments
once until you release all keys
</p>
<div class="counter">Increment: {{ incrementCount }}</div>
<p class="hint">
This prevents repeated triggering while holding the keys down.
Release all keys to allow re-triggering.
</p>
<pre class="code-block">{{ requireResetCode }}</pre>
</section>
<section class="demo-section">
<h2>Conditional Hotkey</h2>
<p>
<kbd>{{ formatForDisplay('Mod+E') }}</kbd> is currently
<strong> {{ enabled ? 'enabled' : 'disabled' }}</strong>
</p>
<button @click="enabled = !enabled">
{{ enabled ? 'Disable' : 'Enable' }} Hotkey
</button>
<pre class="code-block">{{ conditionalCode }}</pre>
</section>
<section class="demo-section">
<h2>Number Key Combinations</h2>
<p>Common for tab/section switching:</p>
<div class="hotkey-grid">
<div>
<kbd>{{ formatForDisplay('Mod+1') }}</kbd> → Tab 1
</div>
<div>
<kbd>{{ formatForDisplay('Mod+2') }}</kbd> → Tab 2
</div>
<div>
<kbd>{{ formatForDisplay('Mod+3') }}</kbd> → Tab 3
</div>
<div>
<kbd>{{ formatForDisplay('Mod+4') }}</kbd> → Tab 4
</div>
<div>
<kbd>{{ formatForDisplay('Mod+5') }}</kbd> → Tab 5
</div>
</div>
<div class="counter">Active Tab: {{ activeTab }}</div>
<pre class="code-block">{{ numberCode }}</pre>
</section>
<section class="demo-section">
<h2>Navigation Key Combinations</h2>
<p>Selection and navigation shortcuts:</p>
<div class="hotkey-grid">
<div>
<kbd>{{ formatForDisplay('Shift+ArrowUp') }}</kbd> — Select up
</div>
<div>
<kbd>{{ formatForDisplay('Shift+ArrowDown') }}</kbd> — Select down
</div>
<div>
<kbd>{{ formatForDisplay('Alt+ArrowLeft') }}</kbd> — Navigate back
</div>
<div>
<kbd>{{ formatForDisplay('Alt+ArrowRight') }}</kbd> — Navigate
forward
</div>
<div>
<kbd>{{ formatForDisplay('Mod+Home') }}</kbd> — Go to start
</div>
<div>
<kbd>{{ formatForDisplay('Mod+End') }}</kbd> — Go to end
</div>
<div>
<kbd>{{ formatForDisplay('Control+PageUp') }}</kbd> — Previous
page
</div>
<div>
<kbd>{{ formatForDisplay('Control+PageDown') }}</kbd> — Next page
</div>
</div>
<div class="counter">
Navigation triggered: {{ navigationCount }}x
</div>
<pre class="code-block">{{ navigationCode }}</pre>
</section>
<section class="demo-section">
<h2>Function Key Combinations</h2>
<p>System and application shortcuts:</p>
<div class="hotkey-grid">
<div>
<kbd>{{ formatForDisplay('Alt+F4') }}</kbd> — Close window
</div>
<div>
<kbd>{{ formatForDisplay('Control+F5') }}</kbd> — Hard refresh
</div>
<div>
<kbd>{{ formatForDisplay('Mod+F1') }}</kbd> — Help
</div>
<div>
<kbd>{{ formatForDisplay('Shift+F10') }}</kbd> — Context menu
</div>
<div>
<kbd>{{ formatForDisplay('F12') }}</kbd> — DevTools
</div>
</div>
<div class="counter">
Function keys triggered: {{ functionKeyCount }}x
</div>
<pre class="code-block">{{ functionCode }}</pre>
</section>
<section class="demo-section">
<h2>Multi-Modifier Combinations</h2>
<p>Complex shortcuts with multiple modifiers:</p>
<div class="hotkey-grid">
<div>
<kbd>{{ formatForDisplay('Mod+Shift+S') }}</kbd> — Save As
</div>
<div>
<kbd>{{ formatForDisplay('Mod+Shift+Z') }}</kbd> — Redo
</div>
<div>
<kbd>{{ formatForDisplay('Control+Alt+A') }}</kbd> — Special
action
</div>
<div>
<kbd>{{ formatForDisplay('Control+Shift+N') }}</kbd> — New
incognito
</div>
<div>
<kbd>{{ formatForDisplay('Mod+Alt+T') }}</kbd> — Toggle theme
</div>
<div>
<kbd>{{ formatForDisplay('Control+Alt+Shift+X') }}</kbd> — Triple
modifier
</div>
</div>
<div class="counter">
Multi-modifier triggered: {{ multiModifierCount }}x
</div>
<pre class="code-block">{{ multiModifierCode }}</pre>
</section>
<section class="demo-section">
<h2>Editing Key Combinations</h2>
<p>Text editing and form shortcuts:</p>
<div class="hotkey-grid">
<div>
<kbd>{{ formatForDisplay('Mod+Enter') }}</kbd> — Submit form
</div>
<div>
<kbd>{{ formatForDisplay('Shift+Enter') }}</kbd> — New line
</div>
<div>
<kbd>{{ formatForDisplay('Mod+Backspace') }}</kbd> — Delete word
</div>
<div>
<kbd>{{ formatForDisplay('Mod+Delete') }}</kbd> — Delete forward
</div>
<div>
<kbd>{{ formatForDisplay('Control+Tab') }}</kbd> — Next tab
</div>
<div>
<kbd>{{ formatForDisplay('Shift+Tab') }}</kbd> — Previous field
</div>
<div>
<kbd>{{ formatForDisplay('Mod+Space') }}</kbd> — Toggle
</div>
</div>
<div class="counter">
Editing keys triggered: {{ editingKeyCount }}x
</div>
<pre class="code-block">{{ editingCode }}</pre>
</section>
<div v-if="lastHotkey" class="info-box">
<strong>Last triggered:</strong> {{ formatForDisplay(lastHotkey) }}
</div>
<p class="hint">Press <kbd>Escape</kbd> to reset all counters</p>
<section class="demo-section scoped-section">
<h2>Scoped Keyboard Shortcuts</h2>
<p>
Shortcuts can be scoped to specific DOM elements using the
<code>target</code> option. This allows different shortcuts to work
in different parts of your application.
</p>
<div class="scoped-grid">
<div ref="sidebarRef" class="scoped-area" tabindex="0">
<h3>Sidebar (Scoped Area)</h3>
<p>Click here to focus, then try:</p>
<div class="hotkey-list">
<div>
<kbd>{{ formatForDisplay('Mod+B') }}</kbd> — Trigger sidebar
action
</div>
<div>
<kbd>{{ formatForDisplay('Mod+N') }}</kbd> — New item
</div>
</div>
<div class="counter">
Sidebar shortcuts: {{ sidebarShortcutCount }}x
</div>
<p class="hint">
These shortcuts only work when this sidebar area is focused or
contains focus.
</p>
</div>
<div class="scoped-area">
<h3>Modal Dialog</h3>
<button @click="modalOpen = true">Open Modal</button>
<div
v-if="modalOpen"
class="modal-overlay"
@click="modalOpen = false"
>
<div
ref="modalRef"
class="modal-content"
tabindex="0"
@click.stop
>
<h3>Modal Dialog (Scoped)</h3>
<p>Try these shortcuts while modal is open:</p>
<div class="hotkey-list">
<div>
<kbd>{{ formatForDisplay('Escape') }}</kbd> — Close modal
</div>
<div>
<kbd>{{ formatForDisplay('Mod+Enter') }}</kbd> — Submit
</div>
</div>
<div class="counter">
Modal shortcuts: {{ modalShortcutCount }}x
</div>
<p class="hint">
These shortcuts only work when the modal is open and
focused. The Escape key here won't conflict with the global
Escape handler.
</p>
<button @click="modalOpen = false">Close</button>
</div>
</div>
</div>
<div class="scoped-area">
<h3>Text Editor (Scoped)</h3>
<p>Focus the editor below and try:</p>
<div class="hotkey-list">
<div>
<kbd>{{ formatForDisplay('Mod+S') }}</kbd> — Save editor
content
</div>
<div>
<kbd>{{ formatForDisplay('Mod+/') }}</kbd> — Add comment
</div>
<div>
<kbd>{{ formatForDisplay('Mod+K') }}</kbd> — Clear editor
</div>
</div>
<textarea
ref="editorRef"
v-model="editorContent"
class="scoped-editor"
placeholder="Focus here and try the shortcuts above..."
rows="8"
/>
<div class="counter">
Editor shortcuts: {{ editorShortcutCount }}x
</div>
<p class="hint">
These shortcuts only work when the editor is focused. Notice
that
<kbd>{{ formatForDisplay('Mod+S') }}</kbd> here doesn't conflict
with the global <kbd>{{ formatForDisplay('Mod+S') }}</kbd>
shortcut.
</p>
</div>
</div>
<pre class="code-block">{{ scopedCode }}</pre>
</section>
</main>
<TanStackDevtools :plugins="plugins" />
</div>
</HotkeysProvider>
</template>