Obsidian Hello World
Overview
Build a minimal working Obsidian plugin demonstrating the five core building blocks: commands (palette + editor + checkCallback), settings tab with typed config, ribbon icons, modals, and status bar. Every snippet uses real Obsidian API.
Prerequisites
- Completed
obsidian-install-auth setup (symlinked dev vault, npm run dev working)
- Build pipeline producing
main.js from src/main.ts
Instructions
Step 1: Define Typed Settings
// src/main.ts — top of file
import {
App, Editor, MarkdownView, Modal, Notice,
Plugin, PluginSettingTab, Setting, TFile
} from 'obsidian';
interface MyPluginSettings {
greeting: string;
showRibbon: boolean;
dateFormat: string;
}
const DEFAULT_SETTINGS: MyPluginSettings = {
greeting: 'Hello, Obsidian!',
showRibbon: true,
dateFormat: 'YYYY-MM-DD',
};
Step 2: Create the Plugin Class with Commands
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
async onload() {
await this.loadSettings();
// Ribbon icon — shows greeting as Notice
if (this.settings.showRibbon) {
this.addRibbonIcon('sparkles', 'My Plugin: Greet', () => {
new Notice(this.settings.greeting);
});
}
// Command: show greeting (available everywhere)
this.addCommand({
id: 'show-greeting',
name: 'Show greeting',
callback: () => new Notice(this.settings.greeting),
});
// Command: insert greeting at cursor (editor-only — greyed out when no editor is active)
this.addCommand({
id: 'insert-greeting',
name: 'Insert greeting at cursor',
editorCallback: (editor: Editor, view: MarkdownView) => {
editor.replaceSelection(this.settings.greeting);
},
});
// Command: word count with checkCallback (conditionally available)
this.addCommand({
id: 'count-words',
name: 'Count words in current note',
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
if (!checking) {
const text = view.editor.getValue();
const count = text.split(/\s+/).filter(Boolean).length;
new Notice(`Word count: ${count}`);
}
return true; // command is available
}
return false; // hide from palette when no editor
},
});
// Command: open modal dialog
this.addCommand({
id: 'show-greeting-modal',
name: 'Show greeting modal',
callback: () => new GreetingModal(this.app, this.settings.greeting).open(),
});
// Command: insert today's date
this.addCommand({
id: 'insert-date',
name: 'Insert today's date',
editorCallback: (editor: Editor) => {
const today = new Date().toISOString().slice(0, 10);
editor.replaceSelection(today);
},
});
// Status bar — persistent widget at bottom
const statusEl = this.addStatusBarItem();
statusEl.setText('Plugin loaded');
// Update status bar when active file changes
this.registerEvent(
this.app.workspace.on('active-leaf-change', () => {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
const count = view.editor.getValue().split(/\s+/).filter(Boolean).length;
statusEl.setText(`Words: ${count}`);
} else {
statusEl.setText('No editor');
}
})
);
// Settings tab
this.addSettingTab(new MySettingTab(this.app, this));
console.log(`[${this.manifest.id}] loaded`);
}
onunload() {
console.log(`[${this.manifest.id}] unloaded`);
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
Step 3: Create Settings Tab
class MySettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName('Greeting message')
.setDesc('Text shown by the greet command and ribbon icon.')
.addText(text => text
.setPlaceholder('Hello, Obsidian!')
.setValue(this.plugin.settings.greeting)
.onChange(async (value) => {
this.plugin.settings.greeting = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Show ribbon icon')
.setDesc('Toggle the sparkles icon in the left ribbon. Reload plugin to apply.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showRibbon)
.onChange(async (value) => {
this.plugin.settings.showRibbon = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Date format')
.setDesc('Format for the Insert Date command.')
.addDropdown(dropdown => dropdown
.addOption('YYYY-MM-DD', '2026-03-22')
.addOption('MM/DD/YYYY', '03/22/2026')
.addOption('DD.MM.YYYY', '22.03.2026')
.setValue(this.plugin.settings.dateFormat)
.onChange(async (value) => {
this.plugin.settings.dateFormat = value;
await this.plugin.saveSettings();
}));
}
}
Step 4: Create a Modal
class GreetingModal extends Modal {
message: string;
constructor(app: App, message: string) {
super(app);
this.message = message;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: this.message });
contentEl.createEl('p', { text: 'This is a modal dialog from your plugin.' });
// Add a button that does something
const btn = contentEl.createEl('button', { text: 'Count vault files' });
btn.addEventListener('click', () => {
const count = this.app.vault.getMarkdownFiles().length;
contentEl.createEl('p', { text: `Your vault has ${count} markdown files.` });
});
}
onClose() {
this.contentEl.empty();
}
}
Step 5: Build and Test
set -euo pipefail
npm run build
# In Obsidian:
# 1. Settings > Community plugins > Enable your plugin
# 2. Click the sparkles icon in the ribbon
# 3. Ctrl+P > "Show greeting"
# 4. Ctrl+P > "Count words in current note" (open a .md file first)
# 5. Ctrl+P > "Show greeting modal"
# 6. Settings > My Plugin > change the greeting
# 7. Check the status bar at bottom for word count
Step 6: Listen to Vault Events
// Add to onload() — react to file changes
this.registerEvent(
this.app.workspace.on('file-open', (file: TFile | null) => {
if (file) {
console.log(`[${this.manifest.id}] Opened: ${file.path}`);
}
})
);
// Track file modifications (debounce for production — see obsidian-rate-limits)
this.registerEvent(
this.app.vault.on('create', (file) => {
if (file instanceof TFile) {
new Notice(`New file: ${file.basename}`);
}
})
);
Output
- Working plugin with:
- Three command types:
callback, editorCallback, checkCallback
- Settings tab with text, toggle, and dropdown controls
- Ribbon icon with click handler
- Modal dialog with interactive button
- Status bar widget with live word count
- Event listeners for file-open and file-create
Error Handling
| Error |
Cause |
Solution |
| Plugin not loading |
Build errors or bad manifest |
Check console (Ctrl+Shift+I) for red errors |
| Settings not saving |
Missing await on saveData |
Always await this.saveSettings() in onChange |
| Command greyed out |
editorCallback needs active editor |
Open a markdown note, or use callback instead |
| Ribbon icon missing |
Invalid icon name |
Use Lucide icon names: sparkles, file-text, search |
| Status bar not updating |
Event not registered |
Wrap in this.registerEvent() for auto-cleanup |
| Settings reset on restart |
Forgot saveData call |
loadData returns null on first run — Object.assign handles this |
Examples
Available Lucide Icon Names
Obsidian uses Lucide icons. Common examples:
file-text, folder, search, settings, star
heart, bookmark, tag, link, external-link
edit, trash-2, copy, clipboard, check
dice, bot, sparkles, wand, calendar
bar-chart-2, globe, download, upload
Command Types Summary
| Type |
When Available |
Use Case |
callback |
Always |
Non-editor commands (open modal, toggle feature) |
editorCallback |
When editor is active |
Insert text, transform selection |
checkCallback |
Conditionally |
Show/hide based on context |
Register a Hotkey-Ready Command
// Users assign hotkeys in Settings > Hotkeys
this.addCommand({
id: 'toggle-feature',
name: 'Toggle my feature',
callback: () => this.toggleFeature(),
});
Resources
Next Steps
- Set up hot-reload development:
obsidian-local-dev-loop
- Build advanced UI (views, fuzzy search, context menus):
obsidian-core-workflow-b
- Apply production patterns:
obsidian-sdk-patterns
1---2name: obsidian-hello-world3description: Create a minimal working Obsidian plugin with commands, settings, modals, and ribbon icons. Use when building your first plugin feature, testing your setup, or learning basic Obsidian plugin patterns. Trigger with phrases like "obsidian hello world", "first obsidian plugin", "obsidian quick start", "simple obsidian plugin".4license: MIT5---6# Obsidian Hello World
7
8## Overview
9
10Build a minimal working Obsidian plugin demonstrating the five core building blocks: commands (palette + editor + checkCallback), settings tab with typed config, ribbon icons, modals, and status bar. Every snippet uses real Obsidian API.
11
12## Prerequisites
13
14- Completed `obsidian-install-auth` setup (symlinked dev vault, `npm run dev` working)
15- Build pipeline producing `main.js` from `src/main.ts`
16
17## Instructions
18
19### Step 1: Define Typed Settings
20
21```typescript
22// src/main.ts — top of file
23import {
24 App, Editor, MarkdownView, Modal, Notice,
25 Plugin, PluginSettingTab, Setting, TFile
26} from 'obsidian';
27
28interface MyPluginSettings {
29 greeting: string;
30 showRibbon: boolean;
31 dateFormat: string;
32}
33
34const DEFAULT_SETTINGS: MyPluginSettings = {
35 greeting: 'Hello, Obsidian!',
36 showRibbon: true,
37 dateFormat: 'YYYY-MM-DD',
38};
39```
40
41### Step 2: Create the Plugin Class with Commands
42
43```typescript
44export default class MyPlugin extends Plugin {
45 settings: MyPluginSettings;
46
47 async onload() {
48 await this.loadSettings();
49
50 // Ribbon icon — shows greeting as Notice
51 if (this.settings.showRibbon) {
52 this.addRibbonIcon('sparkles', 'My Plugin: Greet', () => {
53 new Notice(this.settings.greeting);
54 });
55 }
56
57 // Command: show greeting (available everywhere)
58 this.addCommand({
59 id: 'show-greeting',
60 name: 'Show greeting',
61 callback: () => new Notice(this.settings.greeting),
62 });
63
64 // Command: insert greeting at cursor (editor-only — greyed out when no editor is active)
65 this.addCommand({
66 id: 'insert-greeting',
67 name: 'Insert greeting at cursor',
68 editorCallback: (editor: Editor, view: MarkdownView) => {
69 editor.replaceSelection(this.settings.greeting);
70 },
71 });
72
73 // Command: word count with checkCallback (conditionally available)
74 this.addCommand({
75 id: 'count-words',
76 name: 'Count words in current note',
77 checkCallback: (checking: boolean) => {
78 const view = this.app.workspace.getActiveViewOfType(MarkdownView);
79 if (view) {
80 if (!checking) {
81 const text = view.editor.getValue();
82 const count = text.split(/\s+/).filter(Boolean).length;
83 new Notice(`Word count: ${count}`);
84 }
85 return true; // command is available
86 }
87 return false; // hide from palette when no editor
88 },
89 });
90
91 // Command: open modal dialog
92 this.addCommand({
93 id: 'show-greeting-modal',
94 name: 'Show greeting modal',
95 callback: () => new GreetingModal(this.app, this.settings.greeting).open(),
96 });
97
98 // Command: insert today's date
99 this.addCommand({
100 id: 'insert-date',
101 name: 'Insert today's date',
102 editorCallback: (editor: Editor) => {
103 const today = new Date().toISOString().slice(0, 10);
104 editor.replaceSelection(today);
105 },
106 });
107
108 // Status bar — persistent widget at bottom
109 const statusEl = this.addStatusBarItem();
110 statusEl.setText('Plugin loaded');
111
112 // Update status bar when active file changes
113 this.registerEvent(
114 this.app.workspace.on('active-leaf-change', () => {
115 const view = this.app.workspace.getActiveViewOfType(MarkdownView);
116 if (view) {
117 const count = view.editor.getValue().split(/\s+/).filter(Boolean).length;
118 statusEl.setText(`Words: ${count}`);
119 } else {
120 statusEl.setText('No editor');
121 }
122 })
123 );
124
125 // Settings tab
126 this.addSettingTab(new MySettingTab(this.app, this));
127 console.log(`[${this.manifest.id}] loaded`);
128 }
129
130 onunload() {
131 console.log(`[${this.manifest.id}] unloaded`);
132 }
133
134 async loadSettings() {
135 this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
136 }
137
138 async saveSettings() {
139 await this.saveData(this.settings);
140 }
141}
142```
143
144### Step 3: Create Settings Tab
145
146```typescript
147class MySettingTab extends PluginSettingTab {
148 plugin: MyPlugin;
149
150 constructor(app: App, plugin: MyPlugin) {
151 super(app, plugin);
152 this.plugin = plugin;
153 }
154
155 display(): void {
156 const { containerEl } = this;
157 containerEl.empty();
158
159 new Setting(containerEl)
160 .setName('Greeting message')
161 .setDesc('Text shown by the greet command and ribbon icon.')
162 .addText(text => text
163 .setPlaceholder('Hello, Obsidian!')
164 .setValue(this.plugin.settings.greeting)
165 .onChange(async (value) => {
166 this.plugin.settings.greeting = value;
167 await this.plugin.saveSettings();
168 }));
169
170 new Setting(containerEl)
171 .setName('Show ribbon icon')
172 .setDesc('Toggle the sparkles icon in the left ribbon. Reload plugin to apply.')
173 .addToggle(toggle => toggle
174 .setValue(this.plugin.settings.showRibbon)
175 .onChange(async (value) => {
176 this.plugin.settings.showRibbon = value;
177 await this.plugin.saveSettings();
178 }));
179
180 new Setting(containerEl)
181 .setName('Date format')
182 .setDesc('Format for the Insert Date command.')
183 .addDropdown(dropdown => dropdown
184 .addOption('YYYY-MM-DD', '2026-03-22')
185 .addOption('MM/DD/YYYY', '03/22/2026')
186 .addOption('DD.MM.YYYY', '22.03.2026')
187 .setValue(this.plugin.settings.dateFormat)
188 .onChange(async (value) => {
189 this.plugin.settings.dateFormat = value;
190 await this.plugin.saveSettings();
191 }));
192 }
193}
194```
195
196### Step 4: Create a Modal
197
198```typescript
199class GreetingModal extends Modal {
200 message: string;
201
202 constructor(app: App, message: string) {
203 super(app);
204 this.message = message;
205 }
206
207 onOpen() {
208 const { contentEl } = this;
209 contentEl.createEl('h2', { text: this.message });
210 contentEl.createEl('p', { text: 'This is a modal dialog from your plugin.' });
211
212 // Add a button that does something
213 const btn = contentEl.createEl('button', { text: 'Count vault files' });
214 btn.addEventListener('click', () => {
215 const count = this.app.vault.getMarkdownFiles().length;
216 contentEl.createEl('p', { text: `Your vault has ${count} markdown files.` });
217 });
218 }
219
220 onClose() {
221 this.contentEl.empty();
222 }
223}
224```
225
226### Step 5: Build and Test
227
228```bash
229set -euo pipefail
230npm run build
231
232# In Obsidian:
233# 1. Settings > Community plugins > Enable your plugin
234# 2. Click the sparkles icon in the ribbon
235# 3. Ctrl+P > "Show greeting"
236# 4. Ctrl+P > "Count words in current note" (open a .md file first)
237# 5. Ctrl+P > "Show greeting modal"
238# 6. Settings > My Plugin > change the greeting
239# 7. Check the status bar at bottom for word count
240```
241
242### Step 6: Listen to Vault Events
243
244```typescript
245// Add to onload() — react to file changes
246this.registerEvent(
247 this.app.workspace.on('file-open', (file: TFile | null) => {
248 if (file) {
249 console.log(`[${this.manifest.id}] Opened: ${file.path}`);
250 }
251 })
252);
253
254// Track file modifications (debounce for production — see obsidian-rate-limits)
255this.registerEvent(
256 this.app.vault.on('create', (file) => {
257 if (file instanceof TFile) {
258 new Notice(`New file: ${file.basename}`);
259 }
260 })
261);
262```
263
264## Output
265
266- Working plugin with:
267 - Three command types: `callback`, `editorCallback`, `checkCallback`
268 - Settings tab with text, toggle, and dropdown controls
269 - Ribbon icon with click handler
270 - Modal dialog with interactive button
271 - Status bar widget with live word count
272 - Event listeners for file-open and file-create
273
274## Error Handling
275
276| Error | Cause | Solution |
277|-------|-------|----------|
278| Plugin not loading | Build errors or bad manifest | Check console (Ctrl+Shift+I) for red errors |
279| Settings not saving | Missing `await` on `saveData` | Always `await this.saveSettings()` in `onChange` |
280| Command greyed out | `editorCallback` needs active editor | Open a markdown note, or use `callback` instead |
281| Ribbon icon missing | Invalid icon name | Use Lucide icon names: `sparkles`, `file-text`, `search` |
282| Status bar not updating | Event not registered | Wrap in `this.registerEvent()` for auto-cleanup |
283| Settings reset on restart | Forgot `saveData` call | `loadData` returns null on first run — `Object.assign` handles this |
284
285## Examples
286
287### Available Lucide Icon Names
288
289Obsidian uses [Lucide icons](https://lucide.dev/icons/). Common examples:
290
291- `file-text`, `folder`, `search`, `settings`, `star`
292- `heart`, `bookmark`, `tag`, `link`, `external-link`
293- `edit`, `trash-2`, `copy`, `clipboard`, `check`
294- `dice`, `bot`, `sparkles`, `wand`, `calendar`
295- `bar-chart-2`, `globe`, `download`, `upload`
296
297### Command Types Summary
298
299| Type | When Available | Use Case |
300|------|---------------|----------|
301| `callback` | Always | Non-editor commands (open modal, toggle feature) |
302| `editorCallback` | When editor is active | Insert text, transform selection |
303| `checkCallback` | Conditionally | Show/hide based on context |
304
305### Register a Hotkey-Ready Command
306
307```typescript
308// Users assign hotkeys in Settings > Hotkeys
309this.addCommand({
310 id: 'toggle-feature',
311 name: 'Toggle my feature',
312 callback: () => this.toggleFeature(),
313});
314```
315
316## Resources
317
318- [Obsidian Plugin API](https://docs.obsidian.md/Reference/TypeScript+API)
319- [Plugin Development Workflow](https://docs.obsidian.md/Plugins/Getting+started/Development+workflow)
320- [Lucide Icons](https://lucide.dev/icons/) — icon names for `addRibbonIcon`
321- [Obsidian Hub](https://publish.obsidian.md/hub/) — community knowledge base
322
323## Next Steps
324
325- Set up hot-reload development: `obsidian-local-dev-loop`
326- Build advanced UI (views, fuzzy search, context menus): `obsidian-core-workflow-b`
327- Apply production patterns: `obsidian-sdk-patterns`