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
Basic Plugin Structure
Basic Plugin Structure
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');
}
}
One-Time Initialization
One-Time Initialization
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
Reading and Writing Files
Reading and Writing 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');
Working with Frontmatter
Working with Frontmatter
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);
File Attachments
File Attachments
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
Editor Commands
Editor Commands
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;
}
});
Active Editor Access
Active Editor Access
// 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
Register Events
Register Events
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)
);
}
}
External Settings Changes
External Settings Changes
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
Working with Leaves
Working with Leaves
// 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();
}
Custom Views
Custom Views
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
Settings Tab
Settings Tab
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);
}
}
Modals & Suggestions
Modals & Suggestions
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);
}
}
Icons & Tooltips
Icons & Tooltips
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
Working with Metadata Cache
Working with 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
Dynamic Commands
Dynamic Commands
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);
}
}
Fuzzy Search
Fuzzy Search
import { prepareFuzzySearch } from 'obsidian';
// Create fuzzy search function
const fuzzy = prepareFuzzySearch('query');
// Search items
const items = ['apple', 'application', 'apply', 'banana'];
const results = items
.map(item => ({ item, match: fuzzy(item) }))
.filter(result => result.match !== null)
.sort((a, b) => b.match!.score - a.match!.score);
console.log('Fuzzy results:', results);
Page Preview Integration
Page Preview Integration
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