The keyboard handling system in Obsidian consists of the Keymap class for managing scopes and the related types for handling keyboard events.
Overview
Obsidian’s keyboard system uses a scope-based approach where:
- Multiple scopes can exist in a stack
- Only the top scope receives keyboard events
- Scopes can inherit hotkeys from parent scopes
- Modals and other UI components use their own scopes
Keymap Class
The Keymap class manages the lifecycle of different keyboard scopes.
pushScope
Push a scope onto the scope stack, making it the active scope.
pushScope(scope: Scope): void
Example
const customScope = new Scope();
customScope.register(['Mod'], 'K', () => {
console.log('Hotkey triggered!');
return false;
});
this.app.keymap.pushScope(customScope);
popScope
Remove a scope from the scope stack.
popScope(scope: Scope): void
Example
// Later, when done with the scope
this.app.keymap.popScope(customScope);
Static Methods
isModifier
Check if a modifier key is pressed during an event.
static isModifier(evt: MouseEvent | TouchEvent | KeyboardEvent, modifier: Modifier): boolean
evt
MouseEvent | TouchEvent | KeyboardEvent
required
The event to check
The modifier to check for: ‘Mod’, ‘Ctrl’, ‘Meta’, ‘Shift’, or ‘Alt’
Whether the modifier is pressed
Example
this.registerDomEvent(document, 'click', (evt) => {
if (Keymap.isModifier(evt, 'Mod')) {
console.log('Cmd/Ctrl is pressed');
}
if (Keymap.isModifier(evt, 'Shift')) {
console.log('Shift is pressed');
}
});
isModEvent
Translate an event into the type of pane that should open.
static isModEvent(evt?: UserEvent | null): PaneType | boolean
The event to check (MouseEvent, KeyboardEvent, etc.)
Returns:
'tab' if Cmd/Ctrl is pressed OR if this is a middle-click
'split' if Cmd/Ctrl+Alt is pressed
'window' if Cmd/Ctrl+Alt+Shift is pressed
false if no modifiers are pressed
Example
this.registerDomEvent(linkEl, 'click', (evt) => {
const openMode = Keymap.isModEvent(evt);
if (openMode === 'tab') {
// Open in new tab
this.app.workspace.getLeaf('tab').openFile(file);
} else if (openMode === 'split') {
// Open in split
this.app.workspace.getLeaf('split').openFile(file);
} else if (openMode === 'window') {
// Open in new window
this.app.workspace.getLeaf('window').openFile(file);
} else {
// Open in current pane
this.app.workspace.getLeaf(false).openFile(file);
}
});
Modifier Type
The Modifier type defines the available keyboard modifiers:
type Modifier = 'Mod' | 'Ctrl' | 'Meta' | 'Shift' | 'Alt'
Automatically translates to Cmd on macOS and Ctrl on other platforms
The Ctrl key on all platforms
The Cmd key on macOS and Windows key on other platforms
The Alt key (Option on macOS)
KeymapEventHandler
Represents a registered keyboard event handler.
interface KeymapEventHandler extends KeymapInfo {
scope: Scope;
}
The scope this handler belongs to
The modifiers for this hotkey (inherited from KeymapInfo)
The key for this hotkey (inherited from KeymapInfo)
KeymapEventListener
The callback function type for keyboard events.
type KeymapEventListener = (evt: KeyboardEvent, ctx: KeymapContext) => false | any
Context information about the keymap event
Return false to automatically call preventDefault() on the event
KeymapContext
Provides context information about a keyboard event.
interface KeymapContext extends KeymapInfo {
vkey: string;
}
The interpreted virtual key
The modifiers pressed during the event
Hotkey Interface
Defines a hotkey configuration.
interface Hotkey {
modifiers: Modifier[];
key: string;
}
Array of modifier keys that must be pressed
Complete Example
class MyPlugin extends Plugin {
customScope: Scope;
onload() {
// Create a custom scope
this.customScope = new Scope();
// Register a hotkey in the custom scope
this.customScope.register(['Mod', 'Shift'], 'K', (evt, ctx) => {
console.log('Custom hotkey pressed!');
console.log('Virtual key:', ctx.vkey);
new Notice('Custom hotkey triggered!');
return false; // preventDefault
});
// Add a command with a default hotkey
this.addCommand({
id: 'open-custom-modal',
name: 'Open custom modal',
hotkeys: [{ modifiers: ['Mod'], key: 'K' }],
callback: () => {
this.openCustomModal();
}
});
// Handle clicks with modifier detection
this.registerDomEvent(document, 'click', (evt) => {
const target = evt.target as HTMLElement;
if (target.hasClass('my-custom-link')) {
evt.preventDefault();
const openMode = Keymap.isModEvent(evt);
this.handleLinkClick(openMode);
}
});
}
openCustomModal() {
const modal = new Modal(this.app);
// Push custom scope when modal opens
this.app.keymap.pushScope(this.customScope);
modal.onClose = () => {
// Pop scope when modal closes
this.app.keymap.popScope(this.customScope);
};
modal.open();
}
handleLinkClick(openMode: PaneType | boolean) {
if (openMode === 'tab') {
console.log('Open in new tab');
} else if (openMode === 'split') {
console.log('Open in split');
} else if (openMode === 'window') {
console.log('Open in new window');
} else {
console.log('Open in current pane');
}
}
onunload() {
// Clean up scope if still active
this.app.keymap.popScope(this.customScope);
}
}
Best Practices
Always use 'Mod' instead of platform-specific modifiers to ensure cross-platform compatibility:
// Good
scope.register(['Mod'], 'K', handler);
// Bad - not cross-platform
scope.register(['Ctrl'], 'K', handler);
Clean Up Scopes
Always remove custom scopes when they’re no longer needed:
class MyModal extends Modal {
scope: Scope;
onOpen() {
this.scope = new Scope();
this.scope.register(['Mod'], 'Enter', () => {
this.submit();
return false;
});
this.app.keymap.pushScope(this.scope);
}
onClose() {
this.app.keymap.popScope(this.scope);
}
}
Use Commands for Global Hotkeys
For global hotkeys, prefer using the addCommand() API instead of manually managing scopes:
this.addCommand({
id: 'my-command',
name: 'My command',
hotkeys: [{ modifiers: ['Mod', 'Shift'], key: 'K' }],
callback: () => {
// Command logic
}
});
See Also
- Scope - For detailed information about the Scope class
- Plugin - For registering commands with hotkeys
- Modal - Modals have their own scopes