Building Your First Obsidian Plugin: A Word Counter Badge (Part 1)
Learn to create an Obsidian plugin from scratch. Build a real-time word counter that displays in the status bar with this practical, hands-on guide.
Obsidian is a powerful knowledge management tool that lets you extend functionality through plugins. In this tutorial, we’ll build a Word Counter Badge that displays real-time word and character counts in the status bar.
This project teaches Obsidian plugin fundamentals while creating something genuinely useful.
What We’ll Build
Our Word Counter Badge plugin will:
- Display word count in Obsidian’s status bar
- Update automatically when you switch notes or edit content
- Show both word and character counts
- Handle edge cases gracefully
Prerequisites
Before starting, ensure you have:
- Node.js (v16 or higher) and npm installed
- Basic TypeScript knowledge (we’ll explain as we go)
- Obsidian installed on your computer
- A code editor (VS Code recommended)
- Git (optional, for cloning the sample plugin)
Understanding Obsidian Plugin Architecture
Obsidian plugins are TypeScript/JavaScript modules that interact with Obsidian’s API. They follow a specific structure:
Key Components:
main.ts: Your plugin’s entry pointmanifest.json: Plugin metadata (name, version, description)Pluginclass: Extends Obsidian’s base Plugin class- Lifecycle hooks:
onload()andonunload()
Setting Up Your Plugin Project
Step 1: Create the Plugin Directory
Create a new folder in your Obsidian vault’s plugins directory:
cd /path/to/your/vault/.obsidian/plugins
mkdir word-counter-badge
cd word-counter-badge
Step 2: Initialize the Project
Create a package.json:
{
"name": "word-counter-badge",
"version": "1.0.0",
"description": "Displays word and character count in the status bar",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
},
"keywords": ["obsidian", "plugin", "word-counter"],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"builtin-modules": "^3.3.0",
"esbuild": "^0.19.9",
"obsidian": "latest",
"tslib": "^2.6.2",
"typescript": "^5.3.3"
}
}
Step 3: Install Dependencies
npm install
Step 4: Configure TypeScript
Create tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES2018",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": ["DOM", "ES5", "ES6", "ES7"],
"skipLibCheck": true
},
"include": ["**/*.ts"]
}
Step 5: Create Build Configuration
Create esbuild.config.mjs:
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
*/
`;
const prod = process.argv[2] === "production";
const context = await esbuild.context({
banner: { js: banner },
entryPoints: ["main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}
Step 6: Create the Manifest
Create manifest.json:
{
"id": "word-counter-badge",
"name": "Word Counter Badge",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Displays word and character count in the status bar",
"author": "Your Name",
"authorUrl": "https://yourwebsite.com",
"isDesktopOnly": false
}
Building the Plugin
Now for the interesting part! Create main.ts:
import { Plugin, MarkdownView } from "obsidian";
export default class WordCounterPlugin extends Plugin {
private statusBarItem: HTMLElement;
async onload() {
console.log("Loading Word Counter Badge plugin");
// Create status bar item
this.statusBarItem = this.addStatusBarItem();
this.statusBarItem.setText("Words: 0 | Chars: 0");
// Update counter when active note changes
this.registerEvent(
this.app.workspace.on("active-leaf-change", () => {
this.updateWordCount();
}),
);
// Update counter when file content changes
this.registerEvent(
this.app.workspace.on("editor-change", () => {
this.updateWordCount();
}),
);
// Initial update
this.updateWordCount();
}
onunload() {
console.log("Unloading Word Counter Badge plugin");
}
private updateWordCount() {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) {
this.statusBarItem.setText("Words: 0 | Chars: 0");
return;
}
const content = view.editor.getValue();
const stats = this.calculateStats(content);
this.statusBarItem.setText(
`Words: ${stats.words} | Chars: ${stats.characters}`,
);
}
private calculateStats(text: string): { words: number; characters: number } {
// Remove markdown syntax for more accurate counting
const cleanText = text
// Remove code blocks
.replace(/```[\s\S]*?```/g, "")
// Remove inline code
.replace(/`[^`]*`/g, "")
// Remove links but keep the text
.replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1")
// Remove images
.replace(/!\[([^\]]*)\]\([^\)]+\)/g, "")
// Remove bold/italic markers
.replace(/[*_]{1,3}/g, "")
// Remove headers
.replace(/^#{1,6}\s+/gm, "")
// Remove blockquotes
.replace(/^>\s+/gm, "");
// Count words (split by whitespace and filter empty strings)
const words = cleanText
.trim()
.split(/\s+/)
.filter((word) => word.length > 0).length;
// Count characters (excluding spaces)
const characters = cleanText.replace(/\s/g, "").length;
return { words, characters };
}
}
Understanding the Code
Let’s break down the key parts:
The Plugin Class
export default class WordCounterPlugin extends Plugin {
Every Obsidian plugin extends the base Plugin class. This gives you access to the Obsidian API and lifecycle hooks.
Status Bar Item
this.statusBarItem = this.addStatusBarItem();
addStatusBarItem() creates a new element in Obsidian’s status bar. This is where we display our word count.
Event Listeners
this.registerEvent(
this.app.workspace.on("active-leaf-change", () => {
this.updateWordCount();
}),
);
We register two events:
active-leaf-change: Fires when you switch to a different noteeditor-change: Fires when you edit the current note’s content
The registerEvent() method ensures listeners are properly cleaned up when the plugin is disabled.
Word Counting Logic
private calculateStats(text: string): { words: number; characters: number }
This method:
- Strips Markdown syntax (code blocks, links, formatting)
- Counts words by splitting on whitespace
- Counts characters excluding spaces
This provides more accurate counts than simply counting all characters in raw Markdown.
Testing Your Plugin
Step 1: Build the Plugin
npm run dev
This starts esbuild in watch mode, automatically rebuilding when you save changes.
Step 2: Enable the Plugin
- Open Obsidian
- Go to Settings → Community plugins
- Disable “Safe mode” (if enabled)
- Click “Browse” and find your plugin in the list
- Enable it
Alternatively, since we’re in the plugins folder:
- Reload Obsidian (Ctrl/Cmd + R)
- Check the console for “Loading Word Counter Badge plugin”
Step 3: Test It
- Open a note and start typing
- Watch the status bar update in real-time
- Switch between notes to verify the count updates
- Test with different Markdown elements (code blocks, links, etc.)
Debugging Tips
Use the Developer Console
Open it with Ctrl/Cmd + Shift + I. Your console.log() statements will appear here.
Check for Errors
If the plugin doesn’t load, check the console for errors. Common issues:
- TypeScript compilation errors
- Missing dependencies
- Incorrect manifest.json
Hot Reload
After making changes:
- Save your files
- Reload Obsidian (Ctrl/Cmd + R)
- Check if changes are reflected
What’s Next
You now have a working Obsidian plugin! In Part 2, we’ll enhance it with:
- Settings panel to customize display options
- Reading time estimation
- Commands for detailed statistics
- Distribution and best practices
This foundation demonstrates the core concepts of Obsidian plugin development:
- Plugin lifecycle: Understanding
onload()andonunload() - Event system: Reacting to workspace changes
- UI integration: Working with the status bar
- TypeScript skills: Type safety and modern JavaScript features
Continue to Part 2 to take your plugin to the next level!