Skip to main content
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
scope
Scope
required
The scope to make active

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
scope
Scope
required
The scope to remove

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
modifier
Modifier
required
The modifier to check for: ‘Mod’, ‘Ctrl’, ‘Meta’, ‘Shift’, or ‘Alt’
return
boolean
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
evt
UserEvent | null
The event to check (MouseEvent, KeyboardEvent, etc.)
return
PaneType | boolean
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'
Mod
string
Automatically translates to Cmd on macOS and Ctrl on other platforms
Ctrl
string
The Ctrl key on all platforms
Meta
string
The Cmd key on macOS and Windows key on other platforms
Shift
string
The Shift key
Alt
string
The Alt key (Option on macOS)

KeymapEventHandler

Represents a registered keyboard event handler.
interface KeymapEventHandler extends KeymapInfo {
  scope: Scope;
}
scope
Scope
The scope this handler belongs to
modifiers
string | null
The modifiers for this hotkey (inherited from KeymapInfo)
key
string | null
The key for this hotkey (inherited from KeymapInfo)

KeymapEventListener

The callback function type for keyboard events.
type KeymapEventListener = (evt: KeyboardEvent, ctx: KeymapContext) => false | any
evt
KeyboardEvent
required
The keyboard event
ctx
KeymapContext
required
Context information about the keymap event
return
false | any
Return false to automatically call preventDefault() on the event

KeymapContext

Provides context information about a keyboard event.
interface KeymapContext extends KeymapInfo {
  vkey: string;
}
vkey
string
The interpreted virtual key
modifiers
string | null
The modifiers pressed during the event
key
string | null
The key pressed

Hotkey Interface

Defines a hotkey configuration.
interface Hotkey {
  modifiers: Modifier[];
  key: string;
}
modifiers
Modifier[]
required
Array of modifier keys that must be pressed
key
string
required
The key that must be pressed (see MDN Key Values)

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

Use ‘Mod’ Instead of ‘Ctrl’ or ‘Meta’

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

Build docs developers (and LLMs) love