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.4.1)
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 |
| 6a |
DOM events |
Use registerDomEvent() on the plugin or owning component |
Pair addEventListener with manual removeEventListener 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.4.0): ui/sentence-case is now enabled (warn) and enforced on inline UI strings — it was disabled in v0.3.0. Use the recommendedWithLocalesEn config to also check English locale files (rules 12–13).
| 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 |
Declarative Settings (1.13.0+)
All four settings-tab rules ship as warn in recommended. Rules 17a/17c/17d read minAppVersion from manifest.json; 17b is not version-gated.
| # |
Rule |
✅ Do |
❌ Don't |
| 17a |
settings-tab/require-display |
Keep display() when minAppVersion < 1.13.0 |
Ship declarative-only settings that render nothing on older Obsidian |
| 17b |
settings-tab/prefer-setting-definitions |
Implement getSettingDefinitions() on every PluginSettingTab |
Rely on display() alone — settings won't appear in 1.13+ global search |
| 17c |
settings-tab/prefer-update-over-display |
Call this.update() to re-render declarative settings |
Call this.display() — it's bypassed when definitions are non-empty |
| 17d |
settings-tab/no-deprecated-display |
Delete display() once minAppVersion >= 1.13.0 and definitions exist |
Leave a dead display() behind (auto-fixable) |
| — |
Settings data |
Keep all persisted data inside plugin.settings |
Store sibling keys via saveData() — auto-persist clobbers them |
Detection caveat: these rules match a bare extends PluginSettingTab only. extends obsidian.PluginSettingTab is out of scope and won't be flagged — but the underlying guidance still applies.
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 (e.g., getSettingDefinitions() requires 1.13.0) |
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 |
| 29a |
Getter capture |
Capture activeDocument in a variable when the same document is needed later |
Call activeDocument at setup and again at cleanup — it follows focus and may return different documents |
| 30 |
Timers |
Use activeWindow.setTimeout(), setInterval(), etc. |
Use bare setTimeout(), setInterval() |
| 31 |
Main workspace UI |
Use this.app.workspace.containerEl.ownerDocument from settings |
Use activeDocument to update main workspace from settings window |
Note (v0.4.0): prefer-active-doc remains disabled by default — the only Obsidian rule shipped as off. Enable it manually for popout window support.
Note (v1.13.0): Settings now open in a new window. activeDocument from settings callbacks points to the settings window, not the main vault. Use this.app.workspace.containerEl.ownerDocument to target main workspace UI.
Note: activeDocument/activeWindow are dynamic getters that track the focused window. A listener added via activeDocument.addEventListener() at setup cannot reliably be removed via activeDocument.removeEventListener() at cleanup. Prefer registerDomEvent() (rule 6a), which captures the target at registration.
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 |
| 34a |
!important |
Increase selector specificity or use CSS variables |
Use !important — overrides user themes/snippets |
| 34b |
:has selector |
Toggle classes from TypeScript when conditions change |
Use :has — causes broad selector invalidation and performance issues |
Security & Compatibility
| # |
Rule |
✅ Do |
❌ Don't |
| 35 |
DOM creation |
Use Obsidian DOM helpers (createEl(), createDiv(), createSpan(), createSvg(), createFragment()) via prefer-create-el; linter autofixes activeDocument.createElement() → activeWindow.createEl() (v0.4.1) |
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 |
| Deprecated packages |
Replace flagged npm packages with Node.js built-ins (e.g., builtin-modules → import { builtinModules } from "node:module") |
Use packages the scanner flags as replaceable |
Detailed Guidelines
For comprehensive information on specific topics, see the reference files:
Memory Management & Lifecycle
- Using
registerEvent(), addCommand(), registerDomEvent(), registerInterval()
registerDomEvent() vs manual addEventListener (and the activeDocument drift bug)
- 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
- Declarative settings API (1.13+): migration paths, control types, pitfalls
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
- Avoiding
!important (use specificity or CSS variables)
- Avoiding
:has selector (toggle classes from TypeScript instead)
- 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
- Deprecated/replaceable packages (e.g.,
builtin-modules → node:module)
Plugin Submission Requirements
- Repository structure
- Submission process
- Semantic versioning
- Testing checklist
- Additional resources and important notes
Community Plugin Scanner
- What the scanner runs (ESLint rule sets + checks beyond ESLint)
- Scorecard system (Health, Review, Disclosures, improvement tips)
- Version-stamped — the single file to update as the scanner evolves
ESLint Setup Guide
- Complete ESLint config for community scanner compliance
- Why
typescript-eslint recommendedTypeChecked is required
- Common violations and fixes (floating promises, require imports, etc.)
- Popout window compatibility rules
Plugin Submission Validation Workflow
Before submitting a plugin, follow this sequence:
- Run ESLint —
npx eslint . using eslint-plugin-obsidianmd; fix all errors AND warnings (warnings affect your Scorecard)
- 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
- Create GitHub Release — Tag must match
manifest.json version; attach main.js, manifest.json, and styles.css (optional)
- Submit via community.obsidian.md — Sign in, link GitHub account, navigate to Plugins → New plugin, enter repository URL, review Developer policies, and submit
If ESLint reports new errors after fixing, re-run from step 1.
Scorecard System
Published plugins receive a Scorecard visible on community.obsidian.md. The Scorecard affects user trust and discoverability — a poor score deters users from installing.
Key points:
- Aim for 90%+ overall score
- Fix ALL ESLint warnings, not just errors — warnings are publicly visible
- Use
typescript-eslint/recommendedTypeChecked for type-aware checks
- Add GitHub artifact attestation to releases
See Community Plugin Scanner for full details on scanner checks, Health metrics, Review checks, common warnings, and improvement tips.
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?
- Settings: Using declarative
getSettingDefinitions() on 1.13+? All saved data co-located in plugin.settings?
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();
}
});
Declarative Settings (1.13+)
// ✅ CORRECT — getSettingDefinitions() replaces display() on 1.13+
getSettingDefinitions() {
return [
{
name: 'Mode',
control: {
type: 'dropdown',
key: 'mode',
defaultValue: 'fast',
options: { fast: 'Fast', thorough: 'Thorough' },
},
},
];
}
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: obsidian3description: Comprehensive guidelines for Obsidian.md plugin development including ESLint rules from eslint-plugin-obsidianmd v0.4.1, TypeScript best practices, memory management, API usage (requestUrl vs fetch), UI/UX standards, popout window compatibility, community.obsidian.md submission process, and Scorecard optimization. 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- **Command**: Invoke `create-plugin` using your agent's method (`/create-plugin`, `$create-plugin`, or `@create-plugin`)
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.4.1)
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| 6a | DOM events | Use `registerDomEvent()` on the plugin or owning component | Pair `addEventListener` with manual `removeEventListener` cleanup |
41| 7 | View references | Return views/components directly | Store view references in plugin properties or pass plugin as component to `MarkdownRenderer` |
42| 8 | Leaf detachment | Let Obsidian handle leaf cleanup | Call `detachLeavesOfType()` in `onunload` |
43
44### Type Safety
45| # | Rule | ✅ Do | ❌ Don't |
46|---|------|--------|----------|
47| 9 | TFile/TFolder | Use `instanceof` for type checking | Cast to TFile/TFolder; use `any`; use `var` |
48| 10 | DOM instanceof | Use `.instanceOf(T)` for DOM Nodes/UIEvents | Use `instanceof` for cross-window DOM checks |
49
50### UI/UX
51| # | Rule | ✅ Do | ❌ Don't |
52|---|------|--------|----------|
53| 11 | UI text | Sentence case — "Advanced settings" | Title Case — "Advanced Settings" |
54| 12 | JSON locale | Sentence case in JSON locale files (`recommendedWithLocalesEn`) | Title case in locale JSON |
55| 13 | TS/JS locale | Sentence case in TS/JS locale modules | Title case in locale modules |
56
57> **Note (v0.4.0):** `ui/sentence-case` is now enabled (`warn`) and enforced on inline UI strings — it was disabled in v0.3.0. Use the `recommendedWithLocalesEn` config to also check English locale files (rules 12–13).
58| 14 | Command names | Omit "command" in command names/IDs | Include "command" in names/IDs |
59| 15 | Command IDs | Omit plugin ID/name from command IDs/names | Duplicate plugin ID in command IDs |
60| 16 | Hotkeys | No default hotkeys | Set default hotkeys |
61| 17 | Settings headings | Use `.setHeading()` | Create manual HTML headings; use "General", "settings", or plugin name in headings |
62
63### Declarative Settings (1.13.0+)
64
65All four `settings-tab` rules ship as `warn` in `recommended`. Rules 17a/17c/17d read `minAppVersion` from `manifest.json`; 17b is **not** version-gated.
66
67| # | Rule | ✅ Do | ❌ Don't |
68|---|------|--------|----------|
69| 17a | `settings-tab/require-display` | Keep `display()` when `minAppVersion < 1.13.0` | Ship declarative-only settings that render nothing on older Obsidian |
70| 17b | `settings-tab/prefer-setting-definitions` | Implement `getSettingDefinitions()` on every `PluginSettingTab` | Rely on `display()` alone — settings won't appear in 1.13+ global search |
71| 17c | `settings-tab/prefer-update-over-display` | Call `this.update()` to re-render declarative settings | Call `this.display()` — it's bypassed when definitions are non-empty |
72| 17d | `settings-tab/no-deprecated-display` | Delete `display()` once `minAppVersion >= 1.13.0` and definitions exist | Leave a dead `display()` behind (auto-fixable) |
73| — | Settings data | Keep all persisted data inside `plugin.settings` | Store sibling keys via `saveData()` — auto-persist clobbers them |
74
75> **Detection caveat:** these rules match a bare `extends PluginSettingTab` only. `extends obsidian.PluginSettingTab` is out of scope and won't be flagged — but the underlying guidance still applies.
76
77### API Best Practices
78| # | Rule | ✅ Do | ❌ Don't |
79|---|------|--------|----------|
80| 18 | Active file edits | Use Editor API | Use `Vault.modify()` for active file edits |
81| 19 | Background file mods | Use `Vault.process()` | Use `Vault.modify()` for background modifications |
82| 20 | File deletion | Use `FileManager.trashFile()` | Use `Vault.trash()` or `Vault.delete()` directly |
83| 21 | File lookup | Use `Vault.getAbstractFileByPath()` | Iterate all files with `Vault.getFiles().find()` |
84| 22 | User paths | Use `normalizePath()` | Hardcode `.obsidian` path; use raw user paths |
85| 23 | OS detection | Use `Platform` API | Use `navigator.platform`/`userAgent` |
86| 24 | Network requests | Use `requestUrl()` | Use `fetch()` |
87| 25 | Logging | Minimize console logging; none in `onload`/`onunload` in production | Use `console.log` in `onload`/`onunload` |
88| 26 | Input suggest | Use built-in `AbstractInputSuggest` | Copy Liam's `TextInputSuggest` implementation |
89| 27 | API compatibility | Check `minAppVersion` for API availability (e.g., `getSettingDefinitions()` requires 1.13.0) | Use APIs not available in declared minAppVersion |
90| 28 | Language detection | Use Obsidian's `getLanguage()` | Use `localStorage.getItem('language')` or `i18next-browser-languagedetector` |
91
92### Popout Window Compatibility
93| # | Rule | ✅ Do | ❌ Don't |
94|---|------|--------|----------|
95| 29 | Document/Window | Use `activeDocument` and `activeWindow` | Use global `document` and `window` |
96| 29a | Getter capture | Capture `activeDocument` in a variable when the same document is needed later | Call `activeDocument` at setup and again at cleanup — it follows focus and may return different documents |
97| 30 | Timers | Use `activeWindow.setTimeout()`, `setInterval()`, etc. | Use bare `setTimeout()`, `setInterval()` |
98| 31 | Main workspace UI | Use `this.app.workspace.containerEl.ownerDocument` from settings | Use `activeDocument` to update main workspace from settings window |
99
100> **Note (v0.4.0):** `prefer-active-doc` remains disabled by default — the only Obsidian rule shipped as `off`. Enable it manually for popout window support.
101
102> **Note (v1.13.0):** Settings now open in a new window. `activeDocument` from settings callbacks points to the settings window, not the main vault. Use `this.app.workspace.containerEl.ownerDocument` to target main workspace UI.
103
104> **Note:** `activeDocument`/`activeWindow` are dynamic getters that track the focused window. A listener added via `activeDocument.addEventListener()` at setup cannot reliably be removed via `activeDocument.removeEventListener()` at cleanup. Prefer `registerDomEvent()` (rule 6a), which captures the target at registration.
105
106### Event Handling
107| # | Rule | ✅ Do | ❌ Don't |
108|---|------|--------|----------|
109| 31 | Editor drop/paste | Check `evt.defaultPrevented` and call `evt.preventDefault()` | Handle editor-drop/paste without checking defaultPrevented |
110
111### Styling
112| # | Rule | ✅ Do | ❌ Don't |
113|---|------|--------|----------|
114| 32 | CSS variables | Use Obsidian CSS variables for all styling | Hardcode colors, sizes, or spacing |
115| 33 | CSS scope | Scope CSS to plugin containers | Use broad CSS selectors |
116| 34 | Style elements | Use `styles.css` file (`no-forbidden-elements`) | Create `<link>` or `<style>` elements; assign styles via JavaScript |
117| 34a | `!important` | Increase selector specificity or use CSS variables | Use `!important` — overrides user themes/snippets |
118| 34b | `:has` selector | Toggle classes from TypeScript when conditions change | Use `:has` — causes broad selector invalidation and performance issues |
119
120### Security & Compatibility
121| # | Rule | ✅ Do | ❌ Don't |
122|---|------|--------|----------|
123| 35 | DOM creation | Use Obsidian DOM helpers (`createEl()`, `createDiv()`, `createSpan()`, `createSvg()`, `createFragment()`) via `prefer-create-el`; linter autofixes `activeDocument.createElement()` → `activeWindow.createEl()` (v0.4.1) | Use `document.createElement()`, `document.createDocumentFragment()`, etc. |
124| 36 | Node.js modules | Guard Node.js imports with `Platform.isDesktop` check (`no-nodejs-modules`) | Import Node.js modules without platform guard |
125| 37 | iOS compat | Avoid regex lookbehind (iOS < 16.4 incompatibility) | Use regex lookbehind |
126
127### Accessibility (MANDATORY)
128| # | Rule | ✅ Do | ❌ Don't |
129|---|------|--------|----------|
130| 38 | Keyboard access | Make all interactive elements keyboard accessible; Tab through all elements | Create inaccessible interactive elements |
131| 39 | ARIA labels | Provide ARIA labels for icon buttons; use `data-tooltip-position` for tooltips | Use icon buttons without ARIA labels |
132| 40 | Focus indicators | Use `:focus-visible` with Obsidian CSS variables; touch targets ≥ 44×44px | Remove focus indicators; make touch targets < 44×44px |
133
134### Code Quality
135| Rule | ✅ Do | ❌ Don't |
136|------|--------|----------|
137| Sample code | Remove all sample/template code | Keep class names like MyPlugin, SampleModal |
138| Object.assign | `Object.assign({}, defaults, overrides)` (`object-assign`) | `Object.assign(defaultsVar, other)` — mutates defaults |
139| LICENSE | Copyright holder must not be "Dynalist Inc."; year must be current (`validate-license`) | Leave "Dynalist Inc." as holder or use an outdated year |
140| Async | Use async/await | Use Promise chains |
141| Deprecated packages | Replace flagged npm packages with Node.js built-ins (e.g., `builtin-modules` → `import { builtinModules } from "node:module"`) | Use packages the scanner flags as replaceable |
142
143---
144
145## Detailed Guidelines
146
147For comprehensive information on specific topics, see the reference files:
148
149### [Memory Management & Lifecycle](reference/memory-management.md)
150- Using `registerEvent()`, `addCommand()`, `registerDomEvent()`, `registerInterval()`
151- `registerDomEvent()` vs manual `addEventListener` (and the `activeDocument` drift bug)
152- Avoiding view references in plugin
153- Not using plugin as component
154- Proper leaf cleanup
155
156### [Type Safety](reference/type-safety.md)
157- Using `instanceof` instead of type casting
158- Avoiding `any` type
159- Using `const` and `let` over `var`
160
161### [UI/UX Standards](reference/ui-ux.md)
162- Sentence case enforcement (TypeScript, JSON locale, TS/JS locale modules)
163- `recommendedWithLocalesEn` config for locale file checks
164- Command naming conventions (no "command", no plugin name, no plugin ID)
165- Settings and configuration best practices
166- Declarative settings API (1.13+): migration paths, control types, pitfalls
167
168### [File & Vault Operations](reference/file-operations.md)
169- View access patterns
170- Editor vs Vault API
171- Atomic file operations
172- File management
173- Path handling
174
175### [CSS Styling Best Practices](reference/css-styling.md)
176- Avoiding inline styles
177- Using Obsidian CSS variables
178- Avoiding `!important` (use specificity or CSS variables)
179- Avoiding `:has` selector (toggle classes from TypeScript instead)
180- Scoping plugin styles
181- Theme support
182- Spacing and layout
183
184### [Accessibility (A11y)](reference/accessibility.md)
185- Keyboard navigation (MANDATORY)
186- ARIA labels and roles (MANDATORY)
187- Tooltips and accessibility
188- Focus management (MANDATORY)
189- Focus visible styles (MANDATORY)
190- Screen reader support (MANDATORY)
191- Mobile and touch accessibility (MANDATORY)
192- Accessibility checklist
193
194### [Code Quality & Best Practices](reference/code-quality.md)
195- Removing sample code
196- Security best practices
197- Platform compatibility
198- API usage best practices
199- Async/await patterns
200- DOM helpers
201- Deprecated/replaceable packages (e.g., `builtin-modules` → `node:module`)
202
203### [Plugin Submission Requirements](reference/submission.md)
204- Repository structure
205- Submission process
206- Semantic versioning
207- Testing checklist
208- Additional resources and important notes
209
210### [Community Plugin Scanner](reference/community-scanner.md)
211- What the scanner runs (ESLint rule sets + checks beyond ESLint)
212- Scorecard system (Health, Review, Disclosures, improvement tips)
213- Version-stamped — the single file to update as the scanner evolves
214
215### [ESLint Setup Guide](reference/eslint-setup.md)
216- Complete ESLint config for community scanner compliance
217- Why `typescript-eslint` recommendedTypeChecked is required
218- Common violations and fixes (floating promises, require imports, etc.)
219- Popout window compatibility rules
220
221---
222
223## Plugin Submission Validation Workflow
224
225Before submitting a plugin, follow this sequence:
226
2271. **Run ESLint** — `npx eslint .` using `eslint-plugin-obsidianmd`; fix all errors AND warnings (warnings affect your Scorecard)
2282. **Validate manifest** — Confirm `id`, `name`, `description`, `version`, and `minAppVersion` meet naming and formatting rules (rules 1–5)
2293. **Check LICENSE** — Copyright holder must not be "Dynalist Inc." and the year must be current
2304. **Test on mobile** — Verify no regex lookbehind, no `fetch()`, and touch targets ≥ 44×44px (skip only if plugin is declared desktop-only)
2315. **Keyboard accessibility audit** — Tab through all interactive elements; confirm focus indicators and ARIA labels are present
2326. **Create GitHub Release** — Tag must match `manifest.json` version; attach `main.js`, `manifest.json`, and `styles.css` (optional)
2337. **Submit via community.obsidian.md** — Sign in, link GitHub account, navigate to Plugins → New plugin, enter repository URL, review Developer policies, and submit
234
235If ESLint reports new errors after fixing, re-run from step 1.
236
237---
238
239## Scorecard System
240
241Published plugins receive a **Scorecard** visible on community.obsidian.md. The Scorecard affects user trust and discoverability — a poor score deters users from installing.
242
243**Key points:**
244- Aim for 90%+ overall score
245- Fix ALL ESLint warnings, not just errors — warnings are publicly visible
246- Use `typescript-eslint/recommendedTypeChecked` for type-aware checks
247- Add GitHub artifact attestation to releases
248
249See [Community Plugin Scanner](reference/community-scanner.md) for full details on scanner checks, Health metrics, Review checks, common warnings, and improvement tips.
250
251---
252
253## When Reviewing/Writing Code
254
255Use this checklist for code review and implementation:
256
2571. **Memory management**: Are components and views properly managed?
2582. **Type safety**: Using `instanceof` instead of casts?
2593. **UI text**: Is everything in sentence case?
2604. **Command naming**: No redundant words?
2615. **File operations**: Using preferred APIs?
2626. **Mobile compatibility**: No iOS-incompatible features?
2637. **Sample code**: Removed all boilerplate?
2648. **Manifest**: Correct version, valid structure?
2659. **Accessibility**: Keyboard navigation, ARIA labels, focus indicators?
26610. **Testing**: Can you use the plugin without a mouse?
26711. **Touch targets**: Are all interactive elements at least 44×44px?
26812. **Focus styles**: Using `:focus-visible` and proper CSS variables?
26913. **Settings**: Using declarative `getSettingDefinitions()` on 1.13+? All saved data co-located in `plugin.settings`?
270
271---
272
273## Common Patterns
274
275### Proper Command Registration
276
277```typescript
278// ✅ CORRECT
279this.addCommand({
280 id: 'insert-timestamp',
281 name: 'Insert timestamp',
282 editorCallback: (editor: Editor, view: MarkdownView) => {
283 editor.replaceSelection(new Date().toISOString());
284 }
285});
286```
287
288### Safe Type Narrowing
289
290```typescript
291// ✅ CORRECT
292const file = this.app.vault.getAbstractFileByPath(path);
293if (file instanceof TFile) {
294 // TypeScript now knows it's a TFile
295 await this.app.vault.read(file);
296}
297```
298
299### Keyboard Accessible Button
300
301```typescript
302// ✅ CORRECT
303const button = containerEl.createEl('button', {
304 attr: {
305 'aria-label': 'Open settings',
306 'data-tooltip-position': 'top'
307 }
308});
309button.setText('⚙️');
310
311button.addEventListener('keydown', (e) => {
312 if (e.key === 'Enter' || e.key === ' ') {
313 e.preventDefault();
314 performAction();
315 }
316});
317```
318
319### Declarative Settings (1.13+)
320
321```typescript
322// ✅ CORRECT — getSettingDefinitions() replaces display() on 1.13+
323getSettingDefinitions() {
324 return [
325 {
326 name: 'Mode',
327 control: {
328 type: 'dropdown',
329 key: 'mode',
330 defaultValue: 'fast',
331 options: { fast: 'Fast', thorough: 'Thorough' },
332 },
333 },
334 ];
335}
336```
337
338### Themed CSS
339
340```css
341/* ✅ CORRECT */
342.my-plugin-modal {
343 background: var(--modal-background);
344 color: var(--text-normal);
345 padding: var(--size-4-4);
346 border-radius: var(--radius-m);
347 font-size: var(--font-ui-medium);
348}
349
350.my-plugin-button:focus-visible {
351 outline: 2px solid var(--interactive-accent);
352 outline-offset: 2px;
353}
354```
355
356---
357
358When 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.