| name | obsidian-plugin-development |
| description | Obsidian plugin development best practices, ESLint rules, submission requirements, and API patterns. Auto-loads when working on Obsidian plugin code. |
Obsidian Plugin Development Skill
Comprehensive guidelines for developing high-quality Obsidian plugins that follow best practices, pass code review, and adhere to official submission guidelines.
Top 20 Critical Rules
Submission & Naming (Bot-Enforced)
- Plugin ID: no "obsidian", can't end with "plugin", lowercase only
- Plugin name: no "Obsidian", can't end with "Plugin"
- Plugin name: can't start with "Obsi" or end with "dian"
- Description: no "Obsidian", "This plugin", etc.
- Description must end with
.?!) punctuation
Memory & Lifecycle
- Use
registerEvent() for automatic cleanup
- Don't store view references in plugin class
- Use
instanceof instead of type casting
UI/UX
- Use sentence case for all UI text
- No "command" in command names/IDs
- No plugin ID in command IDs
- No default hotkeys - let users set their own
- Use
.setHeading() for settings section headings
API Best Practices
- Use Editor API for active file edits (
editor.replaceRange())
- Use
Vault.process() for background file modifications
- Use
normalizePath() for user-provided paths
- Use
Platform API for OS detection
- Use
requestUrl() instead of fetch()
- No
console.log in onload/onunload in production
Styling
- Use Obsidian CSS variables
- Scope CSS to plugin containers
Accessibility (MANDATORY)
- Make all interactive elements keyboard accessible
- Provide ARIA labels for icon buttons
- Define clear focus indicators (
:focus-visible)
Security & Compatibility
- Don't use
innerHTML/outerHTML - use DOM API
- Avoid regex lookbehind (iOS incompatible)
- Remove all sample/template code before submission
Event Listeners & Timers
element.addEventListener('click', handler);
setTimeout(() => {}, 1000);
setInterval(() => {}, 5000);
this.registerDomEvent(element, 'click', handler);
this.registerInterval(window.setInterval(() => {}, 5000));
File Operations
const file = this.app.vault.getAbstractFileByPath(path);
await this.app.vault.modify(file, content);
await this.app.vault.read(file);
const found = this.app.vault.getMarkdownFiles().find(f => f.path === path);
const file = this.app.vault.getFileByPath(path);
await this.app.vault.process(file, (content) => newContent);
await this.app.vault.cachedRead(file);
const found = this.app.vault.getFileByPath(path);
Workspace & Views
const leaf = this.app.workspace.activeLeaf;
const view = leaf.view;
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
const editor = view.editor;
}
Network Requests
const response = await fetch(url);
import { requestUrl } from 'obsidian';
const response = await requestUrl({ url });
DOM & Security
element.innerHTML = userContent;
element.outerHTML = template;
const div = element.createEl('div');
div.setText(userContent);
div.addClass('my-class');
Type Safety
const file = abstractFile as TFile;
(component as any).internalMethod();
if (abstractFile instanceof TFile) {
const file = abstractFile;
}
Command Registration
this.addCommand({
id: 'my-plugin-open-command',
name: 'Open Command',
hotkeys: [{ modifiers: ['Mod'], key: 'o' }],
});
this.addCommand({
id: 'open',
name: 'Open sidebar',
callback: () => { }
});
Settings UI
containerEl.createEl('h2', { text: 'My Plugin Settings' });
new Setting(containerEl).setName('Enable Feature');
new Setting(containerEl)
.setHeading()
.setName('General');
new Setting(containerEl)
.setName('Enable feature')
.setDesc('Enables the main feature')
.addToggle(toggle => toggle.setValue(this.settings.enabled));
Plugin Lifecycle
class MyPlugin extends Plugin {
view: CustomView;
async onload() {
this.view = new CustomView();
}
onunload() {
this.app.workspace.detachLeavesOfType(VIEW_TYPE);
}
}
class MyPlugin extends Plugin {
async onload() {
this.registerView(VIEW_TYPE, (leaf) => {
return new CustomView(leaf);
});
}
onunload() {
}
}
Accessibility (MANDATORY)
button.setAttribute('aria-label', 'Close sidebar');
button.setAttribute('tabindex', '0');
const iconBtn = containerEl.createEl('button', { cls: 'clickable-icon' });
setIcon(iconBtn, 'x');
iconBtn.setAttribute('aria-label', 'Close');
button.style.minWidth = '44px';
button.style.minHeight = '44px';
.my-button:focus-visible {
outline: 2px solid var(--interactive-accent);
outline-offset: 2px;
}
CSS Best Practices
.sidebar { background: #ffffff; color: #000000; }
.nova-sidebar {
background: var(--background-primary);
color: var(--text-normal);
border: 1px solid var(--background-modifier-border);
padding: var(--size-4-2);
}
--background-primary
--background-secondary
--text-normal
--text-muted
--text-faint
--interactive-accent
--background-modifier-border
--background-modifier-hover
For automated compliance auditing, use the compliance-checker agent or /project:compliance command.