Skip to main content
The Scope class represents a keyboard event scope that receives keyboard events and binds callbacks to hotkeys. Only one scope is active at a time, but scopes can inherit hotkeys from parent scopes.

Overview

Scopes are used throughout Obsidian to manage keyboard input:
  • The main app has a global scope
  • Modals create their own scope when opened
  • Custom UI elements can create scopes for context-specific hotkeys
  • Scopes are managed in a stack via the Keymap class

Constructor

const scope = new Scope(parent?: Scope);
parent
Scope
Optional parent scope to inherit hotkeys from

Example

// Create a scope with no parent
const scope = new Scope();

// Create a scope that inherits from app scope
const childScope = new Scope(this.app.scope);

Methods

register

Register a keyboard event handler in this scope.
register(
  modifiers: Modifier[] | null,
  key: string | null,
  func: KeymapEventListener
): KeymapEventHandler
modifiers
Modifier[] | null
required
Array of modifiers (‘Mod’, ‘Ctrl’, ‘Meta’, ‘Shift’, ‘Alt’) or null to match any modifiers
key
string | null
required
The key to bind to (see MDN Key Values), or null to match any key
func
KeymapEventListener
required
Callback function that will be called when the hotkey is triggered. Return false to automatically call preventDefault()
return
KeymapEventHandler
A handler reference that can be used to unregister the hotkey

Examples

Basic Hotkey Registration
const scope = new Scope();

// Register Mod+K
scope.register(['Mod'], 'K', (evt, ctx) => {
  console.log('Mod+K pressed');
  new Notice('Hotkey triggered!');
  return false; // preventDefault
});

// Register Mod+Shift+P
scope.register(['Mod', 'Shift'], 'P', () => {
  console.log('Command palette shortcut');
  return false;
});
Catch-All Handlers
// Catch any key press (no modifiers)
scope.register(null, null, (evt, ctx) => {
  console.log('Any key pressed:', ctx.vkey);
  // Don't return false - let other handlers process it
});

// Catch any key with Mod pressed
scope.register(['Mod'], null, (evt, ctx) => {
  console.log('Mod + any key:', ctx.key);
});
Using Event Context
scope.register(['Mod'], 'Enter', (evt, ctx) => {
  console.log('Key:', ctx.key);
  console.log('Virtual key:', ctx.vkey);
  console.log('Modifiers:', ctx.modifiers);
  
  // Access the original keyboard event
  if (evt.repeat) {
    console.log('Key is being held down');
  }
  
  return false;
});

unregister

Remove a keyboard event handler from this scope.
unregister(handler: KeymapEventHandler): void
handler
KeymapEventHandler
required
The handler reference returned by register()

Example

const scope = new Scope();

// Register and keep the handler reference
const handler = scope.register(['Mod'], 'K', () => {
  console.log('Handler called');
  return false;
});

// Later, unregister the handler
scope.unregister(handler);

Working with Scopes

Activating and Deactivating Scopes

Scopes must be pushed onto the keymap stack to become active:
class MyPlugin extends Plugin {
  customScope: Scope;
  
  onload() {
    this.customScope = new Scope();
    
    this.customScope.register(['Mod'], 'K', () => {
      new Notice('Custom hotkey!');
      return false;
    });
    
    // Activate the scope
    this.app.keymap.pushScope(this.customScope);
  }
  
  onunload() {
    // Deactivate the scope
    this.app.keymap.popScope(this.customScope);
  }
}

Scope Inheritance

Scopes can inherit hotkeys from a parent scope:
class MyModal extends Modal {
  onOpen() {
    // Create a scope that inherits from the modal's scope
    const customScope = new Scope(this.scope);
    
    // Add custom hotkeys
    customScope.register(['Mod'], 'Enter', () => {
      this.submit();
      return false;
    });
    
    customScope.register([], 'Escape', () => {
      this.close();
      return false;
    });
    
    // Push the custom scope
    this.app.keymap.pushScope(customScope);
    
    // Don't forget to pop it when done
    this.onClose = () => {
      this.app.keymap.popScope(customScope);
    };
  }
}
Modals automatically have a scope that you can use:
class MyModal extends Modal {
  onOpen() {
    // The modal already has a scope available
    this.scope.register(['Mod'], 'Enter', () => {
      this.submit();
      return false;
    });
    
    // Escape key is already handled by the modal
    // but you can override it if needed
  }
  
  submit() {
    // Handle form submission
    this.close();
  }
}

Complete Examples

Custom Input Handler

class CustomInputModal extends Modal {
  result: string = '';
  
  onOpen() {
    const { contentEl, scope } = this;
    
    contentEl.createEl('h2', { text: 'Enter text' });
    
    const inputEl = contentEl.createEl('input', {
      type: 'text',
      placeholder: 'Type something...'
    });
    
    // Focus the input
    inputEl.focus();
    
    // Register Mod+Enter to submit
    scope.register(['Mod'], 'Enter', () => {
      this.result = inputEl.value;
      this.close();
      return false;
    });
    
    // Register Escape to cancel
    scope.register([], 'Escape', () => {
      this.result = '';
      this.close();
      return false;
    });
    
    // Register arrow keys for navigation
    scope.register([], 'ArrowUp', () => {
      console.log('Up arrow pressed');
      return false;
    });
    
    scope.register([], 'ArrowDown', () => {
      console.log('Down arrow pressed');
      return false;
    });
  }
}

Temporary Scope for Feature

class MyPlugin extends Plugin {
  specialModeScope: Scope | null = null;
  
  enableSpecialMode() {
    if (this.specialModeScope) return;
    
    // Create a new scope
    this.specialModeScope = new Scope();
    
    // Register special mode hotkeys
    this.specialModeScope.register([], 'j', () => {
      console.log('j - move down');
      return false;
    });
    
    this.specialModeScope.register([], 'k', () => {
      console.log('k - move up');
      return false;
    });
    
    this.specialModeScope.register([], 'Escape', () => {
      this.disableSpecialMode();
      return false;
    });
    
    // Activate the scope
    this.app.keymap.pushScope(this.specialModeScope);
    
    new Notice('Special mode enabled (press Escape to exit)');
  }
  
  disableSpecialMode() {
    if (!this.specialModeScope) return;
    
    // Deactivate and clean up
    this.app.keymap.popScope(this.specialModeScope);
    this.specialModeScope = null;
    
    new Notice('Special mode disabled');
  }
}

Context-Sensitive Hotkeys

class MyPlugin extends Plugin {
  onload() {
    // Add to main app scope
    this.app.scope.register(['Mod', 'Shift'], 'D', (evt) => {
      const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
      
      if (!activeView) {
        new Notice('No active markdown view');
        return false;
      }
      
      const editor = activeView.editor;
      const selection = editor.getSelection();
      
      if (selection) {
        // Duplicate the selection
        editor.replaceSelection(selection + '\n' + selection);
      } else {
        // Duplicate the line
        const cursor = editor.getCursor();
        const line = editor.getLine(cursor.line);
        editor.replaceRange('\n' + line, { line: cursor.line, ch: 0 });
      }
      
      return false;
    });
  }
}

Best Practices

Always Clean Up Scopes

Remember to pop scopes when you’re done with them:
// Good
class MyFeature {
  scope: Scope;
  
  enable() {
    this.scope = new Scope();
    // Register handlers...
    app.keymap.pushScope(this.scope);
  }
  
  disable() {
    app.keymap.popScope(this.scope);
  }
}

Use Appropriate Modifier Keys

Use 'Mod' for cross-platform compatibility:
// Good - works on all platforms
scope.register(['Mod'], 'K', handler);

// Bad - platform-specific
scope.register(['Ctrl'], 'K', handler);

Return false to preventDefault

Return false from your handler to automatically prevent default behavior:
scope.register(['Mod'], 'S', (evt, ctx) => {
  // Handle save
  saveDocument();
  return false; // Prevents browser's save dialog
});

Use Modal/Component Scopes

When working with modals, use the built-in scope:
class MyModal extends Modal {
  onOpen() {
    // Use this.scope, don't create a new one
    this.scope.register(['Mod'], 'Enter', () => {
      this.submit();
      return false;
    });
  }
}

See Also

  • Keymap - For managing scope stacks
  • Modal - Modals have built-in scopes
  • Plugin - For registering global commands with hotkeys

Build docs developers (and LLMs) love