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
- Slash command:
/create-plugin for guided setup
- 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.1.9)
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 |
Type Safety
| # |
Rule |
✅ Do |
❌ Don't |
| 8 |
TFile/TFolder |
Use instanceof for type checking |
Cast to TFile/TFolder; use any; use var |
UI/UX
| # |
Rule |
✅ Do |
❌ Don't |
| 9 |
UI text |
Sentence case — "Advanced settings" |
Title Case — "Advanced Settings" |
| 10 |
JSON locale |
Sentence case in JSON locale files (recommendedWithLocalesEn) |
Title case in locale JSON |
| 11 |
TS/JS locale |
Sentence case in TS/JS locale modules |
Title case in locale modules |
| 12 |
Command names |
Omit "command" in command names/IDs |
Include "command" in names/IDs |
| 13 |
Command IDs |
Omit plugin ID/name from command IDs/names |
Duplicate plugin ID in command IDs |
| 14 |
Hotkeys |
No default hotkeys |
Set default hotkeys |
| 15 |
Settings headings |
Use .setHeading() |
Create manual HTML headings; use "General", "settings", or plugin name in headings |
API Best Practices
| # |
Rule |
✅ Do |
❌ Don't |
| 16 |
Active file edits |
Use Editor API |
Use Vault.modify() for active file edits |
| 17 |
Background file mods |
Use Vault.process() |
Use Vault.modify() for background modifications |
| 18 |
User paths |
Use normalizePath() |
Hardcode .obsidian path; use raw user paths |
| 19 |
OS detection |
Use Platform API |
Use navigator.platform/userAgent |
| 20 |
Network requests |
Use requestUrl() |
Use fetch() |
| 21 |
Logging |
Minimize console logging; none in onload/onunload in production |
Use console.log in onload/onunload |
Styling
| # |
Rule |
✅ Do |
❌ Don't |
| 22 |
CSS variables |
Use Obsidian CSS variables for all styling |
Hardcode colors, sizes, or spacing |
| 23 |
CSS scope |
Scope CSS to plugin containers |
Use broad CSS selectors |
| 24 |
Style elements |
Use styles.css file (no-forbidden-elements) |
Create <link> or <style> elements; assign styles via JavaScript |
Accessibility (MANDATORY)
| # |
Rule |
✅ Do |
❌ Don't |
| 25 |
Keyboard access |
Make all interactive elements keyboard accessible; Tab through all elements |
Create inaccessible interactive elements |
| 26 |
ARIA labels |
Provide ARIA labels for icon buttons; use data-tooltip-position for tooltips |
Use icon buttons without ARIA labels |
| 27 |
Focus indicators |
Use :focus-visible with Obsidian CSS variables; touch targets ≥ 44×44px |
Remove focus indicators; make touch targets < 44×44px |
Security & Compatibility
| Rule |
✅ Do |
❌ Don't |
| DOM safety |
Use Obsidian DOM helpers (createDiv(), createSpan(), createEl()) |
Use innerHTML/outerHTML or document.createElement |
| iOS compat |
Avoid regex lookbehind (iOS < 16.4 incompatibility) |
Use regex lookbehind |
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:
Memory Management & Lifecycle
- Using
registerEvent(), addCommand(), registerDomEvent(), registerInterval()
- Avoiding view references in plugin
- Not using plugin as component
- Proper leaf cleanup
Type Safety
- Using
instanceof instead of type casting
- Avoiding
any type
- Using
const and let over var
UI/UX Standards
- 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
File & Vault Operations
- View access patterns
- Editor vs Vault API
- Atomic file operations
- File management
- Path handling
CSS Styling Best Practices
- Avoiding inline styles
- Using Obsidian CSS variables
- Scoping plugin styles
- Theme support
- Spacing and layout
Accessibility (A11y)
- Keyboard navigation (MANDATORY)
- ARIA labels and roles (MANDATORY)
- Tooltips and accessibility
- Focus management (MANDATORY)
- Focus visible styles (MANDATORY)
- Screen reader support (MANDATORY)
- Mobile and touch accessibility (MANDATORY)
- Accessibility checklist
Code Quality & Best Practices
- Removing sample code
- Security best practices
- Platform compatibility
- API usage best practices
- Async/await patterns
- DOM helpers
Plugin Submission Requirements
- Repository structure
- Submission process
- Semantic versioning
- Testing checklist
- Additional resources and important notes
Plugin Submission Validation Workflow
Before submitting a plugin, follow this sequence:
- Run ESLint —
npx eslint . using eslint-plugin-obsidianmd; fix all errors (warnings are informational)
- Validate manifest — Confirm
id, name, description, version, and minAppVersion meet naming and formatting rules (rules 1–5)
- Check LICENSE — Copyright holder must not be "Dynalist Inc." and the year must be current
- Test on mobile — Verify no regex lookbehind, no
fetch(), and touch targets ≥ 44×44px (skip only if plugin is declared desktop-only)
- Keyboard accessibility audit — Tab through all interactive elements; confirm focus indicators and ARIA labels are present
- Submit — Open a PR to the community plugins repository with the updated
manifest.json and community-plugins.json entry
If ESLint reports new errors after fixing, re-run from step 1.
When Reviewing/Writing Code
Use this checklist for code review and implementation:
- Memory management: Are components and views properly managed?
- Type safety: Using
instanceof instead of casts?
- UI text: Is everything in sentence case?
- Command naming: No redundant words?
- File operations: Using preferred APIs?
- Mobile compatibility: No iOS-incompatible features?
- Sample code: Removed all boilerplate?
- Manifest: Correct version, valid structure?
- Accessibility: Keyboard navigation, ARIA labels, focus indicators?
- Testing: Can you use the plugin without a mouse?
- Touch targets: Are all interactive elements at least 44×44px?
- Focus styles: Using
:focus-visible and proper CSS variables?
Common Patterns
Proper Command Registration
// ✅ CORRECT
this.addCommand({
id: 'insert-timestamp',
name: 'Insert timestamp',
editorCallback: (editor: Editor, view: MarkdownView) => {
editor.replaceSelection(new Date().toISOString());
}
});
Safe Type Narrowing
// ✅ CORRECT
const file = this.app.vault.getAbstractFileByPath(path);
if (file instanceof TFile) {
// TypeScript now knows it's a TFile
await this.app.vault.read(file);
}
Keyboard Accessible Button
// ✅ CORRECT
const button = containerEl.createEl('button', {
attr: {
'aria-label': 'Open settings',
'data-tooltip-position': 'top'
}
});
button.setText('⚙️');
button.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
performAction();
}
});
Themed CSS
/* ✅ CORRECT */
.my-plugin-modal {
background: var(--modal-background);
color: var(--text-normal);
padding: var(--size-4-4);
border-radius: var(--radius-m);
font-size: var(--font-ui-medium);
}
.my-plugin-button:focus-visible {
outline: 2px solid var(--interactive-accent);
outline-offset: 2px;
}
When helping with Obsidian plugin development, proactively apply these rules and suggest improvements based on these guidelines. Refer to the detailed reference files for comprehensive information on specific topics.
1---2name: obsidian-53description: Comprehensive guidelines for Obsidian.md plugin development including all 27 ESLint rules from eslint-plugin-obsidianmd v0.1.9, TypeScript best practices, memory management, API usage (requestUrl vs fetch), UI/UX standards, locale file sentence-case enforcement, and submission requirements. Use when working with Obsidian plugins, main.ts files, manifest.json, Plugin class, MarkdownView, TFile, vault operations, or any Obsidian API development.4license: MIT5---6
7# Obsidian Plugin Development Guidelines
8
9Follow these comprehensive guidelines derived from the official Obsidian ESLint plugin rules, submission requirements, and best practices.
10
11## Getting Started
12
13### Quick Start Tool
14
15For new plugin projects, an interactive boilerplate generator is available:
16- **Script**: `tools/create-plugin.js` in the skill repository
17- **Slash command**: `/create-plugin` for guided setup
18- Generates minimal, best-practice boilerplate with no sample code
19- Detects existing projects and only adds missing files
20
21Recommend 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.
22
23---
24
25## Rules Reference (eslint-plugin-obsidianmd v0.1.9)
26
27### Submission & Naming
28| # | Rule | ✅ Do | ❌ Don't |
29|---|------|--------|----------|
30| 1 | Plugin ID | Omit "obsidian"; don't end with "plugin" | Include "obsidian" or end with "plugin" |
31| 2 | Plugin name | Omit "Obsidian"; don't end with "Plugin" | Include "Obsidian" or end with "Plugin" |
32| 3 | Plugin name | Don't start with "Obsi" or end with "dian" | Start with "Obsi" or end with "dian" |
33| 4 | Description | Omit "Obsidian", "This plugin", etc. | Use "Obsidian" or "This plugin" |
34| 5 | Description | End with `.?!)` punctuation | Leave description without terminal punctuation |
35
36### Memory & Lifecycle
37| # | Rule | ✅ Do | ❌ Don't |
38|---|------|--------|----------|
39| 6 | Event cleanup | Use `registerEvent()` for automatic cleanup | Register events without cleanup |
40| 7 | View references | Return views/components directly | Store view references in plugin properties or pass plugin as component to `MarkdownRenderer` |
41
42### Type Safety
43| # | Rule | ✅ Do | ❌ Don't |
44|---|------|--------|----------|
45| 8 | TFile/TFolder | Use `instanceof` for type checking | Cast to TFile/TFolder; use `any`; use `var` |
46
47### UI/UX
48| # | Rule | ✅ Do | ❌ Don't |
49|---|------|--------|----------|
50| 9 | UI text | Sentence case — "Advanced settings" | Title Case — "Advanced Settings" |
51| 10 | JSON locale | Sentence case in JSON locale files (`recommendedWithLocalesEn`) | Title case in locale JSON |
52| 11 | TS/JS locale | Sentence case in TS/JS locale modules | Title case in locale modules |
53| 12 | Command names | Omit "command" in command names/IDs | Include "command" in names/IDs |
54| 13 | Command IDs | Omit plugin ID/name from command IDs/names | Duplicate plugin ID in command IDs |
55| 14 | Hotkeys | No default hotkeys | Set default hotkeys |
56| 15 | Settings headings | Use `.setHeading()` | Create manual HTML headings; use "General", "settings", or plugin name in headings |
57
58### API Best Practices
59| # | Rule | ✅ Do | ❌ Don't |
60|---|------|--------|----------|
61| 16 | Active file edits | Use Editor API | Use `Vault.modify()` for active file edits |
62| 17 | Background file mods | Use `Vault.process()` | Use `Vault.modify()` for background modifications |
63| 18 | User paths | Use `normalizePath()` | Hardcode `.obsidian` path; use raw user paths |
64| 19 | OS detection | Use `Platform` API | Use `navigator.platform`/`userAgent` |
65| 20 | Network requests | Use `requestUrl()` | Use `fetch()` |
66| 21 | Logging | Minimize console logging; none in `onload`/`onunload` in production | Use `console.log` in `onload`/`onunload` |
67
68### Styling
69| # | Rule | ✅ Do | ❌ Don't |
70|---|------|--------|----------|
71| 22 | CSS variables | Use Obsidian CSS variables for all styling | Hardcode colors, sizes, or spacing |
72| 23 | CSS scope | Scope CSS to plugin containers | Use broad CSS selectors |
73| 24 | Style elements | Use `styles.css` file (`no-forbidden-elements`) | Create `<link>` or `<style>` elements; assign styles via JavaScript |
74
75### Accessibility (MANDATORY)
76| # | Rule | ✅ Do | ❌ Don't |
77|---|------|--------|----------|
78| 25 | Keyboard access | Make all interactive elements keyboard accessible; Tab through all elements | Create inaccessible interactive elements |
79| 26 | ARIA labels | Provide ARIA labels for icon buttons; use `data-tooltip-position` for tooltips | Use icon buttons without ARIA labels |
80| 27 | Focus indicators | Use `:focus-visible` with Obsidian CSS variables; touch targets ≥ 44×44px | Remove focus indicators; make touch targets < 44×44px |
81
82### Security & Compatibility
83| Rule | ✅ Do | ❌ Don't |
84|------|--------|----------|
85| DOM safety | Use Obsidian DOM helpers (`createDiv()`, `createSpan()`, `createEl()`) | Use `innerHTML`/`outerHTML` or `document.createElement` |
86| iOS compat | Avoid regex lookbehind (iOS < 16.4 incompatibility) | Use regex lookbehind |
87
88### Code Quality
89| Rule | ✅ Do | ❌ Don't |
90|------|--------|----------|
91| Sample code | Remove all sample/template code | Keep class names like MyPlugin, SampleModal |
92| Object.assign | `Object.assign({}, defaults, overrides)` (`object-assign`) | `Object.assign(defaultsVar, other)` — mutates defaults |
93| LICENSE | Copyright holder must not be "Dynalist Inc."; year must be current (`validate-license`) | Leave "Dynalist Inc." as holder or use an outdated year |
94| Async | Use async/await | Use Promise chains |
95
96---
97
98## Detailed Guidelines
99
100For comprehensive information on specific topics, see the reference files:
101
102### [Memory Management & Lifecycle](reference/memory-management.md)
103- Using `registerEvent()`, `addCommand()`, `registerDomEvent()`, `registerInterval()`
104- Avoiding view references in plugin
105- Not using plugin as component
106- Proper leaf cleanup
107
108### [Type Safety](reference/type-safety.md)
109- Using `instanceof` instead of type casting
110- Avoiding `any` type
111- Using `const` and `let` over `var`
112
113### [UI/UX Standards](reference/ui-ux.md)
114- Sentence case enforcement (TypeScript, JSON locale, TS/JS locale modules)
115- `recommendedWithLocalesEn` config for locale file checks
116- Command naming conventions (no "command", no plugin name, no plugin ID)
117- Settings and configuration best practices
118
119### [File & Vault Operations](reference/file-operations.md)
120- View access patterns
121- Editor vs Vault API
122- Atomic file operations
123- File management
124- Path handling
125
126### [CSS Styling Best Practices](reference/css-styling.md)
127- Avoiding inline styles
128- Using Obsidian CSS variables
129- Scoping plugin styles
130- Theme support
131- Spacing and layout
132
133### [Accessibility (A11y)](reference/accessibility.md)
134- Keyboard navigation (MANDATORY)
135- ARIA labels and roles (MANDATORY)
136- Tooltips and accessibility
137- Focus management (MANDATORY)
138- Focus visible styles (MANDATORY)
139- Screen reader support (MANDATORY)
140- Mobile and touch accessibility (MANDATORY)
141- Accessibility checklist
142
143### [Code Quality & Best Practices](reference/code-quality.md)
144- Removing sample code
145- Security best practices
146- Platform compatibility
147- API usage best practices
148- Async/await patterns
149- DOM helpers
150
151### [Plugin Submission Requirements](reference/submission.md)
152- Repository structure
153- Submission process
154- Semantic versioning
155- Testing checklist
156- Additional resources and important notes
157
158---
159
160## Plugin Submission Validation Workflow
161
162Before submitting a plugin, follow this sequence:
163
1641. **Run ESLint** — `npx eslint .` using `eslint-plugin-obsidianmd`; fix all errors (warnings are informational)
1652. **Validate manifest** — Confirm `id`, `name`, `description`, `version`, and `minAppVersion` meet naming and formatting rules (rules 1–5)
1663. **Check LICENSE** — Copyright holder must not be "Dynalist Inc." and the year must be current
1674. **Test on mobile** — Verify no regex lookbehind, no `fetch()`, and touch targets ≥ 44×44px (skip only if plugin is declared desktop-only)
1685. **Keyboard accessibility audit** — Tab through all interactive elements; confirm focus indicators and ARIA labels are present
1696. **Submit** — Open a PR to the community plugins repository with the updated `manifest.json` and `community-plugins.json` entry
170
171If ESLint reports new errors after fixing, re-run from step 1.
172
173---
174
175## When Reviewing/Writing Code
176
177Use this checklist for code review and implementation:
178
1791. **Memory management**: Are components and views properly managed?
1802. **Type safety**: Using `instanceof` instead of casts?
1813. **UI text**: Is everything in sentence case?
1824. **Command naming**: No redundant words?
1835. **File operations**: Using preferred APIs?
1846. **Mobile compatibility**: No iOS-incompatible features?
1857. **Sample code**: Removed all boilerplate?
1868. **Manifest**: Correct version, valid structure?
1879. **Accessibility**: Keyboard navigation, ARIA labels, focus indicators?
18810. **Testing**: Can you use the plugin without a mouse?
18911. **Touch targets**: Are all interactive elements at least 44×44px?
19012. **Focus styles**: Using `:focus-visible` and proper CSS variables?
191
192---
193
194## Common Patterns
195
196### Proper Command Registration
197
198```typescript
199// ✅ CORRECT
200this.addCommand({
201 id: 'insert-timestamp',
202 name: 'Insert timestamp',
203 editorCallback: (editor: Editor, view: MarkdownView) => {
204 editor.replaceSelection(new Date().toISOString());
205 }
206});
207```
208
209### Safe Type Narrowing
210
211```typescript
212// ✅ CORRECT
213const file = this.app.vault.getAbstractFileByPath(path);
214if (file instanceof TFile) {
215 // TypeScript now knows it's a TFile
216 await this.app.vault.read(file);
217}
218```
219
220### Keyboard Accessible Button
221
222```typescript
223// ✅ CORRECT
224const button = containerEl.createEl('button', {
225 attr: {
226 'aria-label': 'Open settings',
227 'data-tooltip-position': 'top'
228 }
229});
230button.setText('⚙️');
231
232button.addEventListener('keydown', (e) => {
233 if (e.key === 'Enter' || e.key === ' ') {
234 e.preventDefault();
235 performAction();
236 }
237});
238```
239
240### Themed CSS
241
242```css
243/* ✅ CORRECT */
244.my-plugin-modal {
245 background: var(--modal-background);
246 color: var(--text-normal);
247 padding: var(--size-4-4);
248 border-radius: var(--radius-m);
249 font-size: var(--font-ui-medium);
250}
251
252.my-plugin-button:focus-visible {
253 outline: 2px solid var(--interactive-accent);
254 outline-offset: 2px;
255}
256```
257
258---
259
260When helping with Obsidian plugin development, proactively apply these rules and suggest improvements based on these guidelines. Refer to the detailed reference files for comprehensive information on specific topics.