Skip to main content
Learn by example with real-world code patterns and the official sample plugin.

Getting Started

Sample Plugin

Official sample plugin template with build setup and best practices

Official Documentation

Complete guides and tutorials for plugin development

Common Patterns

All examples assume you’re working within a class that extends Plugin and have access to this.app.

Plugin Basics

import { Plugin } from 'obsidian';

export default class MyPlugin extends Plugin {
  async onload() {
    console.log('Loading plugin');
    
    // Add ribbon icon
    this.addRibbonIcon('dice', 'Sample Plugin', () => {
      console.log('Ribbon icon clicked');
    });
    
    // Add command
    this.addCommand({
      id: 'sample-command',
      name: 'Sample Command',
      callback: () => {
        console.log('Command executed');
      }
    });
    
    // Load plugin data
    await this.loadData();
  }
  
  onunload() {
    console.log('Unloading plugin');
  }
}
Use onUserEnable for one-time setup when the user first installs your plugin:
export default class MyPlugin extends Plugin {
  async onload() {
    // Runs every time Obsidian starts
    console.log('Plugin loaded');
  }
  
  async onUserEnable() {
    // Only runs once when user first enables the plugin
    console.log('First time setup');
    
    // Initialize custom views
    this.registerView(
      VIEW_TYPE,
      (leaf) => new MyCustomView(leaf)
    );
  }
}

Working with Files

import { TFile } from 'obsidian';

// Get a file by path
const file = this.app.vault.getFileByPath('path/to/file.md');

if (file) {
  // Read file contents
  const content = await this.app.vault.read(file);
  
  // Modify and write back
  const newContent = content + '\n\nNew line added';
  await this.app.vault.modify(file, newContent);
}

// Create a new file
const newFile = await this.app.vault.create(
  'path/to/newfile.md',
  'File contents'
);

// Get folder by path
const folder = this.app.vault.getFolderByPath('path/to/folder');
import { TFile } from 'obsidian';

const file = this.app.vault.getFileByPath('note.md');

if (file) {
  // Atomically update frontmatter
  await this.app.fileManager.processFrontMatter(file, (frontmatter) => {
    // Add or update fields
    frontmatter['tags'] = ['example', 'plugin'];
    frontmatter['created'] = new Date().toISOString();
    frontmatter['count'] = (frontmatter['count'] || 0) + 1;
    
    // Delete fields
    delete frontmatter['oldField'];
  });
}

// Get frontmatter info (offsets)
const content = await this.app.vault.read(file);
const fmInfo = getFrontMatterInfo(content);
console.log('Frontmatter ends at:', fmInfo.contentStart);
import { TFile } from 'obsidian';

// Get a safe path for an attachment
const attachmentPath = this.app.fileManager.getAvailablePathForAttachment(
  'image.png',
  'path/to/note.md'
);

// The path respects user's attachment settings
console.log(attachmentPath); // e.g., "attachments/image.png"

// Create the attachment
await this.app.vault.createBinary(
  attachmentPath,
  arrayBuffer
);

Working with the Editor

this.addCommand({
  id: 'insert-text',
  name: 'Insert Text at Cursor',
  editorCallback: (editor, ctx) => {
    // Get current cursor position
    const cursor = editor.getCursor();
    
    // Insert text at cursor
    editor.replaceRange('Hello, World!', cursor);
    
    // Get selected text
    const selection = editor.getSelection();
    
    // Replace selection
    editor.replaceSelection(`**${selection}**`);
    
    // Get file from context (works in Canvas too!)
    const file = ctx.file;
  }
});
// Works with both markdown views and canvas file cards
const { activeEditor } = this.app.workspace;

if (activeEditor) {
  const editor = activeEditor.editor;
  const file = activeEditor.file;
  
  if (editor && file) {
    const content = editor.getValue();
    console.log(`Editing ${file.path}`);
  }
}

Event Handling

export default class MyPlugin extends Plugin {
  async onload() {
    // Workspace events
    this.registerEvent(
      this.app.workspace.on('file-open', (file) => {
        console.log('File opened:', file?.path);
      })
    );
    
    // Vault events
    this.registerEvent(
      this.app.vault.on('modify', (file) => {
        console.log('File modified:', file.path);
      })
    );
    
    // DOM events
    this.registerDomEvent(document, 'click', (evt) => {
      console.log('Document clicked');
    });
    
    // Intervals
    this.registerInterval(
      window.setInterval(() => {
        console.log('Periodic task');
      }, 5000)
    );
  }
}
export default class MyPlugin extends Plugin {
  settings: MyPluginSettings;
  
  async onload() {
    await this.loadSettings();
    
    // React to external settings changes (e.g., from sync)
    this.onExternalSettingsChange = async () => {
      console.log('Settings changed externally!');
      await this.loadSettings();
      // Update UI or reconfigure plugin
    };
  }
  
  async loadSettings() {
    this.settings = Object.assign(
      {},
      DEFAULT_SETTINGS,
      await this.loadData()
    );
  }
}

Workspace & Views

// Get or create a leaf in the sidebar
const leaf = this.app.workspace.ensureSideLeaf(
  'left', // or 'right'
  'my-view-type'
);

// Open a file in a new tab with modifier key support
this.addCommand({
  id: 'open-file',
  name: 'Open File',
  callback: () => {
    const file = this.app.vault.getFileByPath('note.md');
    if (file) {
      // Opens in new tab/window if modifier key is pressed
      const leaf = this.app.workspace.getLeaf(
        Keymap.isModEvent(evt)
      );
      await leaf.openFile(file);
    }
  }
});

// Handle deferred views (v1.7.2+)
if (leaf.isDeferred) {
  await leaf.loadIfDeferred();
}
import { ItemView, WorkspaceLeaf } from 'obsidian';

const VIEW_TYPE = 'my-custom-view';

class MyCustomView extends ItemView {
  constructor(leaf: WorkspaceLeaf) {
    super(leaf);
  }
  
  getViewType(): string {
    return VIEW_TYPE;
  }
  
  getDisplayText(): string {
    return 'My Custom View';
  }
  
  async onOpen() {
    const container = this.containerEl.children[1];
    container.empty();
    container.createEl('h4', { text: 'My Custom View' });
    
    // Register hotkeys for this view
    this.scope.register(['Mod'], 'k', () => {
      console.log('Hotkey pressed in view');
    });
  }
  
  async onClose() {
    // Clean up
  }
}

// Register the view in your plugin
export default class MyPlugin extends Plugin {
  async onload() {
    this.registerView(
      VIEW_TYPE,
      (leaf) => new MyCustomView(leaf)
    );
    
    // Add command to open the view
    this.addCommand({
      id: 'open-custom-view',
      name: 'Open Custom View',
      callback: () => {
        this.activateView();
      }
    });
  }
  
  async activateView() {
    const { workspace } = this.app;
    
    let leaf = workspace.getLeavesOfType(VIEW_TYPE)[0];
    
    if (!leaf) {
      leaf = workspace.getRightLeaf(false);
      await leaf.setViewState({ type: VIEW_TYPE });
    }
    
    workspace.revealLeaf(leaf);
  }
}

UI Components

import { App, PluginSettingTab, Setting } from 'obsidian';

interface MyPluginSettings {
  apiKey: string;
  enableFeature: boolean;
  maxItems: number;
}

const DEFAULT_SETTINGS: MyPluginSettings = {
  apiKey: '',
  enableFeature: true,
  maxItems: 10
};

class MySettingTab extends PluginSettingTab {
  plugin: MyPlugin;
  
  constructor(app: App, plugin: MyPlugin) {
    super(app, plugin);
    this.plugin = plugin;
  }
  
  display(): void {
    const { containerEl } = this;
    containerEl.empty();
    
    // Text input
    new Setting(containerEl)
      .setName('API Key')
      .setDesc('Enter your API key')
      .addText(text => text
        .setPlaceholder('Enter key')
        .setValue(this.plugin.settings.apiKey)
        .onChange(async (value) => {
          this.plugin.settings.apiKey = value;
          await this.plugin.saveSettings();
        }));
    
    // Toggle
    new Setting(containerEl)
      .setName('Enable Feature')
      .setDesc('Toggle this feature on or off')
      .addToggle(toggle => toggle
        .setValue(this.plugin.settings.enableFeature)
        .onChange(async (value) => {
          this.plugin.settings.enableFeature = value;
          await this.plugin.saveSettings();
        }));
    
    // Slider
    new Setting(containerEl)
      .setName('Max Items')
      .setDesc('Maximum number of items')
      .addSlider(slider => slider
        .setLimits(1, 100, 1)
        .setValue(this.plugin.settings.maxItems)
        .setDynamicTooltip()
        .onChange(async (value) => {
          this.plugin.settings.maxItems = value;
          await this.plugin.saveSettings();
        }));
  }
}

// Register in your plugin
export default class MyPlugin extends Plugin {
  settings: MyPluginSettings;
  
  async onload() {
    await this.loadSettings();
    this.addSettingTab(new MySettingTab(this.app, this));
  }
  
  async loadSettings() {
    this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
  }
  
  async saveSettings() {
    await this.saveData(this.settings);
  }
}
import { Modal, SuggestModal, FuzzySuggestModal } from 'obsidian';

// Basic Modal
class MyModal extends Modal {
  onOpen() {
    const { contentEl } = this;
    contentEl.createEl('h2', { text: 'Modal Title' });
    contentEl.createEl('p', { text: 'Modal content' });
  }
  
  onClose() {
    const { contentEl } = this;
    contentEl.empty();
  }
}

// Suggest Modal
interface MyItem {
  name: string;
  value: string;
}

class MySuggestModal extends SuggestModal<MyItem> {
  items: MyItem[];
  
  constructor(app: App, items: MyItem[]) {
    super(app);
    this.items = items;
  }
  
  getSuggestions(query: string): MyItem[] {
    return this.items.filter(item =>
      item.name.toLowerCase().includes(query.toLowerCase())
    );
  }
  
  renderSuggestion(item: MyItem, el: HTMLElement) {
    el.createEl('div', { text: item.name });
  }
  
  onChooseSuggestion(item: MyItem, evt: MouseEvent | KeyboardEvent) {
    console.log('Selected:', item.value);
  }
  
  // Programmatically select the active suggestion
  selectActive() {
    this.selectActiveSuggestion();
  }
}

// Fuzzy Suggest Modal (for files)
class MyFileSuggestModal extends FuzzySuggestModal<TFile> {
  getItems(): TFile[] {
    return this.app.vault.getMarkdownFiles();
  }
  
  getItemText(file: TFile): string {
    return file.basename;
  }
  
  onChooseItem(file: TFile, evt: MouseEvent | KeyboardEvent) {
    this.app.workspace.openLinkText(file.path, '', true);
  }
}
import { setIcon, setTooltip } from 'obsidian';

// Add icon to element
const iconEl = containerEl.createDiv();
setIcon(iconEl, 'star');

// Add tooltip
const button = containerEl.createEl('button', { text: 'Click me' });
setTooltip(button, 'This is a tooltip');

// Add tooltip with position
setTooltip(button, 'Tooltip text', {
  placement: 'top' // 'top', 'bottom', 'left', 'right'
});

// Custom icon size via CSS
iconEl.addClass('my-icon');
// In CSS:
// .my-icon { --icon-size: var(--icon-l); }
// Available: --icon-xs, --icon-s, --icon-m, --icon-l

Metadata & Cache

import { TFile } from 'obsidian';

const file = this.app.vault.getFileByPath('note.md');

if (file) {
  // Get cached metadata
  const cache = this.app.metadataCache.getFileCache(file);
  
  if (cache) {
    // Access frontmatter
    const frontmatter = cache.frontmatter;
    console.log('Tags:', frontmatter?.tags);
    
    // Access headings
    cache.headings?.forEach(heading => {
      console.log(`H${heading.level}: ${heading.heading}`);
    });
    
    // Access links
    cache.links?.forEach(link => {
      console.log('Link:', link.link);
    });
    
    // Access frontmatter links (v1.4.0+)
    cache.frontmatterLinks?.forEach(link => {
      console.log('Frontmatter link:', link.link);
    });
    
    // Access tags
    cache.tags?.forEach(tag => {
      console.log('Tag:', tag.tag);
    });
  }
  
  // Listen for cache updates
  this.registerEvent(
    this.app.metadataCache.on('changed', (file) => {
      console.log('Metadata changed:', file.path);
    })
  );
}

Advanced Patterns

export default class MyPlugin extends Plugin {
  commands: Map<string, Command> = new Map();
  
  async onload() {
    // Add initial commands
    this.addUserCommand('cmd-1', 'Command 1');
    this.addUserCommand('cmd-2', 'Command 2');
  }
  
  addUserCommand(id: string, name: string) {
    const command = this.addCommand({
      id: `user-${id}`,
      name: name,
      callback: () => {
        console.log(`Executing ${name}`);
      }
    });
    
    this.commands.set(id, command);
  }
  
  removeUserCommand(id: string) {
    const commandId = `user-${id}`;
    this.removeCommand(commandId);
    this.commands.delete(id);
  }
}
import { ItemView, WorkspaceLeaf } from 'obsidian';

const VIEW_TYPE = 'my-view';

class MyView extends ItemView {
  async onOpen() {
    const container = this.containerEl.children[1];
    container.empty();
    
    // Create link element
    const link = container.createEl('a', {
      text: 'Hover for preview',
      href: '#',
      cls: 'internal-link'
    });
    
    // Set link data for page preview
    link.dataset.href = 'path/to/note';
  }
}

export default class MyPlugin extends Plugin {
  async onload() {
    this.registerView(VIEW_TYPE, (leaf) => new MyView(leaf));
    
    // Register view with page preview plugin
    this.registerHoverLinkSource(VIEW_TYPE, {
      display: 'My Custom View',
      defaultMod: true
    });
  }
}

Learn More

API Reference

Explore the complete API documentation

Developer Forum

Ask questions and share your plugins

Plugin Guidelines

Best practices for publishing plugins

Changelog

Stay updated with API changes

Build docs developers (and LLMs) love