brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
obsidianComprehensive guidelines for Obsidian.md plugin development including ESLint rules from eslint-plugin-obsidianmd v0.4.2, 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, mai…
Independent GitHub snapshot · Automated classification · Original documentation governs
Follow these comprehensive guidelines derived from the official Obsidian ESLint plugin rules, submission requirements, and best practices.
For new plugin projects, an interactive boilerplate generator is available:
tools/create-plugin.js in the skill repositorycreate-plugin using your agent's method (/create-plugin, $create-plugin, or @create-plugin)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.
| # | 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 |
| # | 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 |
| # | 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 |
| # | Rule | ✅ Do | ❌ Don't |
|---|---|---|---|
| 11 | UI text | Sentence case — "Advanced settings"; use acronyms/brands options for proper names | 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-caseis now enabled (warn) and enforced on inline UI strings — it was disabled in v0.3.0. Use therecommendedWithLocalesEnconfig 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 |
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 PluginSettingTabonly.extends obsidian.PluginSettingTabis out of scope and won't be flagged — but the underlying guidance still applies.
| # | 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() |
| 24a | Response typing | Parse response.text and validate/cast once at the API boundary | Pass response.json (typed any) into plugin code |
| 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 |
| 26a | Confirm dialogs | Use built-in ConfirmationModal (1.13.0+); use FileManager.promptForDeletion() to confirm and delete files | Hand-build a Modal subclass with its own confirm/cancel button row |
| 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 |
| # | 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 window.setTimeout(), window.setInterval(), window.requestAnimationFrame(), etc. (exception to rule 29) | Use bare setTimeout()/setInterval(), or activeWindow.setTimeout() — prefer-window-timers flags both |
| 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-docremains disabled by default — the only Obsidian rule shipped asoff. Enable it manually for popout window support.
Note (v1.13.0): Settings now open in a new window.
activeDocumentfrom settings callbacks points to the settings window, not the main vault. Usethis.app.workspace.containerEl.ownerDocumentto target main workspace UI.
Note:
activeDocument/activeWindoware dynamic getters that track the focused window. A listener added viaactiveDocument.addEventListener()at setup cannot reliably be removed viaactiveDocument.removeEventListener()at cleanup. PreferregisterDomEvent()(rule 6a), which captures the target at registration.
| # | Rule | ✅ Do | ❌ Don't |
|---|---|---|---|
| 31 | Editor drop/paste | Check evt.defaultPrevented and call evt.preventDefault() | Handle editor-drop/paste without checking defaultPrevented |
| # | 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 |
| # | 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 |
| # | 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 |
| 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 |
For comprehensive information on specific topics, see the reference files:
registerEvent(), addCommand(), registerDomEvent(), registerInterval()registerDomEvent() vs manual addEventListener (and the activeDocument drift bug)instanceof instead of type castingany typeconst and let over varrecommendedWithLocalesEn config for locale file checksConfirmationModal (1.13+), awaiting the user's choice, promptForDeletion()!important (use specificity or CSS variables):has selector (toggle classes from TypeScript instead)builtin-modules → node:module)typescript-eslint recommendedTypeChecked is requiredBefore submitting a plugin, follow this sequence:
npx eslint . using eslint-plugin-obsidianmd; fix all errors AND warnings (warnings affect your Scorecard)id, name, description, version, and minAppVersion meet naming and formatting rules (rules 1–5)fetch(), and touch targets ≥ 44×44px (skip only if plugin is declared desktop-only)manifest.json version; attach main.js, manifest.json, and styles.css (optional)If ESLint reports new errors after fixing, re-run from step 1.
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:
typescript-eslint/recommendedTypeChecked for type-aware checksSee Community Plugin Scanner for full details on scanner checks, Health metrics, Review checks, common warnings, and improvement tips.
Use this checklist for code review and implementation:
instanceof instead of casts?:focus-visible and proper CSS variables?getSettingDefinitions() on 1.13+? All saved data co-located in plugin.settings?ConfirmationModal or promptForDeletion() instead of a custom confirm modal?// ✅ CORRECT
this.addCommand({
id: 'insert-timestamp',
name: 'Insert timestamp',
editorCallback: (editor: Editor, view: MarkdownView) => {
editor.replaceSelection(new Date().toISOString());
}
});
// ✅ 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);
}
// ✅ 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();
}
});
// ✅ CORRECT — getSettingDefinitions() replaces display() on 1.13+
getSettingDefinitions() {
return [
{
name: 'Mode',
control: {
type: 'dropdown',
key: 'mode',
defaultValue: 'fast',
options: { fast: 'Fast', thorough: 'Thorough' },
},
},
];
}
/* ✅ 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.