Obsidian Plugin Development Guidelines
Follow these comprehensive guidelines derived from the official Obsidian ESLint plugin rules, submission requirements, and best practices.
Getting Started
Quick Start Tool
For new plugin projects, an interactive boilerplate generator is available:
- Script:
tools/create-plugin.js in the skill repository
- Command: Invoke
create-plugin using your agent's method (/create-plugin, $create-plugin, or @create-plugin)
- Generates minimal, best-practice boilerplate with no sample code
- Detects existing projects and only adds missing files
Recommend the boilerplate generator when users ask how to create a new plugin, want to start a new project, or need help setting up the basic structure.
Rules Reference (eslint-plugin-obsidianmd v0.3.0)
Submission & Naming
| # | Rule | ✅ Do | ❌ Don't |
|---|
| 1 | Plugin ID | Omit "obsidian"; don't end with "plugin" | Include "obsidian" or end with "plugin" |
| 2 | Plugin name | Omit "Obsidian"; don't end with "Plugin" | Include "Obsidian" or end with "Plugin" |
| 3 | Plugin name | Don't start with "Obsi" or end with "dian" | Start with "Obsi" or end with "dian" |
| 4 | Description | Omit "Obsidian", "This plugin", etc. | Use "Obsidian" or "This plugin" |
| 5 | Description | End with .?!) punctuation | Leave description without terminal punctuation |
Memory & Lifecycle
| # | Rule | ✅ Do | ❌ Don't |
|---|
| 6 | Event cleanup | Use registerEvent() for automatic cleanup | Register events without cleanup |
| 7 | View references | Return views/components directly | Store view references in plugin properties or pass plugin as component to MarkdownRenderer |
| 8 | Leaf detachment | Let Obsidian handle leaf cleanup | Call detachLeavesOfType() in onunload |
Type Safety
| # | Rule | ✅ Do | ❌ Don't |
|---|
| 9 | TFile/TFolder | Use instanceof for type checking | Cast to TFile/TFolder; use any; use var |
| 10 | DOM instanceof | Use .instanceOf(T) for DOM Nodes/UIEvents | Use instanceof for cross-window DOM checks |
UI/UX
| # | Rule | ✅ Do | ❌ Don't |
|---|
| 11 | UI text | Sentence case — "Advanced settings" | Title Case — "Advanced Settings" |
| 12 | JSON locale | Sentence case in JSON locale files (recommendedWithLocalesEn) | Title case in locale JSON |
| 13 | TS/JS locale | Sentence case in TS/JS locale modules | Title case in locale modules |
Note (v0.3.0): The ui/sentence-case rule is disabled by default (not working as intended). Consider enabling manually if needed.
| 14 | Command names | Omit "command" in command names/IDs | Include "command" in names/IDs |
| 15 | Command IDs | Omit plugin ID/name from command IDs/names | Duplicate plugin ID in command IDs |
| 16 | Hotkeys | No default hotkeys | Set default hotkeys |
| 17 | Settings headings | Use .setHeading() | Create manual HTML headings; use "General", "settings", or plugin name in headings |
API Best Practices
| # | Rule | ✅ Do | ❌ Don't |
|---|
| 18 | Active file edits | Use Editor API | Use Vault.modify() for active file edits |
| 19 | Background file mods | Use Vault.process() | Use Vault.modify() for background modifications |
| 20 | File deletion | Use FileManager.trashFile() | Use Vault.trash() or Vault.delete() directly |
| 21 | File lookup | Use Vault.getAbstractFileByPath() | Iterate all files with Vault.getFiles().find() |
| 22 | User paths | Use normalizePath() | Hardcode .obsidian path; use raw user paths |
| 23 | OS detection | Use Platform API | Use navigator.platform/userAgent |
| 24 | Network requests | Use requestUrl() | Use fetch() |
| 25 | Logging | Minimize console logging; none in onload/onunload in production | Use console.log in onload/onunload |
| 26 | Input suggest | Use built-in AbstractInputSuggest | Copy Liam's TextInputSuggest implementation |
| 27 | API compatibility | Check minAppVersion for API availability | Use APIs not available in declared minAppVersion |
| 28 | Language detection | Use Obsidian's getLanguage() | Use localStorage.getItem('language') or i18next-browser-languagedetector |
Popout Window Compatibility
| # | Rule | ✅ Do | ❌ Don't |
|---|
| 29 | Document/Window | Use activeDocument and activeWindow | Use global document and window |
| 30 | Timers | Use activeWindow.setTimeout(), setInterval(), etc. | Use bare setTimeout(), setInterval() |
Note (v0.3.0): The prefer-active-doc rule is disabled by default. Enable manually for popout window support.
Event Handling
| # | Rule | ✅ Do | ❌ Don't |
|---|
| 31 | Editor drop/paste | Check evt.defaultPrevented and call evt.preventDefault() | Handle editor-drop/paste without checking defaultPrevented |
Styling
| # | Rule | ✅ Do | ❌ Don't |
|---|
| 32 | CSS variables | Use Obsidian CSS variables for all styling | Hardcode colors, sizes, or spacing |
| 33 | CSS scope | Scope CSS to plugin containers | Use broad CSS selectors |
| 34 | Style elements | Use styles.css file (no-forbidden-elements) | Create <link> or <style> elements; assign styles via JavaScript |
Security & Compatibility
| # | Rule | ✅ Do | ❌ Don't |
|---|
| 35 | DOM creation | Use Obsidian DOM helpers (createEl(), createDiv(), createSpan(), createSvg(), createFragment()) via prefer-create-el | Use document.createElement(), document.createDocumentFragment(), etc. |
| 36 | Node.js modules | Guard Node.js imports with Platform.isDesktop check (no-nodejs-modules) | Import Node.js modules without platform guard |
| 37 | iOS compat | Avoid regex lookbehind (iOS < 16.4 incompatibility) | Use regex lookbehind |
Accessibility (MANDATORY)
| # | Rule | ✅ Do | ❌ Don't |
|---|
| 38 | Keyboard access | Make all interactive elements keyboard accessible; Tab through all elements | Create inaccessible interactive elements |
| 39 | ARIA labels | Provide ARIA labels for icon buttons; use data-tooltip-position for tooltips | Use icon buttons without ARIA labels |
| 40 | Focus indicators | Use :focus-visible with Obsidian CSS variables; touch targets ≥ 44×44px | Remove focus indicators; make touch targets < 44×44px |
Code Quality
| Rule | ✅ Do | ❌ Don't |
|---|
| Sample code | Remove all sample/template code | Keep class names like MyPlugin, SampleModal |
| Object.assign | Object.assign({}, defaults, overrides) (object-assign) | Object.assign(defaultsVar, other) — mutates defaults |
| LICENSE | Copyright holder must not be "Dynalist Inc."; year must be current (validate-license) | Leave "Dynalist Inc." as holder or use an outdated year |
| Async | Use async/await | Use Promise chains |
Detailed Guidelines
For comprehensive information on specific topics, see the reference files:
- Using
registerEvent(), addCommand(), registerDomEvent(), registerInterval()
- Avoiding view references in plugin
- Not using plugin as component
- Proper leaf cleanup
- Using
instanceof instead of type casting
- Avoiding
any type
- Using
const and let over var
- Sentence case enforcement (TypeScript, JSON locale, TS/JS locale modules)
recommendedWithLocalesEn config for locale file checks
- Command naming conventions (no "command", no plugin name, no plugin ID)
- Settings and configuration best practices
- View access patterns
- Editor vs Vault API
- Atomic file operations
- File management
- Path handling
[CSS Styling Best Practices](reference/cs