Execute Obsidian secondary workflow: UI components and user interaction. Use when building modals, views, suggestions, or custom UI elements. Trigger with phrases like "obsidian modal", "obsidian UI", "obsidian view", "obsidian custom interface".
Scanned 9/3/2026
Install to Claude Code
npx -y skills add Dicklesworthstone/pi_agent_rust --skill obsidian-core-workflow-b --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Obsidian Core Workflow B?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/dicklesworthstone-obsidian-core-workflow-b)More formats (shields.io, HTML) on the badges page.
---
name: obsidian-core-workflow-b
description: |
Execute Obsidian secondary workflow: UI components and user interaction.
Use when building modals, views, suggestions, or custom UI elements.
Trigger with phrases like "obsidian modal", "obsidian UI",
"obsidian view", "obsidian custom interface".
allowed-tools: Read, Write, Edit, Bash(npm:*), Grep
version: 1.0.0
license: MIT
author: Jeremy Longshore <jeremy@intentsolutions.io>
---
# Obsidian Core Workflow B: UI Components
## Overview
Secondary workflow for Obsidian: building modals, views, suggestion popups, and custom UI elements.
## Prerequisites
- Completed `obsidian-install-auth` setup
- Familiarity with `obsidian-core-workflow-a`
- Understanding of DOM manipulation
## Instructions
### Step 1: Modal Dialogs
```typescript
import { App, Modal, Setting } from 'obsidian';
// Simple confirmation modal
export class ConfirmModal extends Modal {
private result: boolean = false;
private onSubmit: (result: boolean) => void;
private message: string;
constructor(app: App, message: string, onSubmit: (result: boolean) => void) {
super(app);
this.message = message;
this.onSubmit = onSubmit;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: 'Confirm Action' });
contentEl.createEl('p', { text: this.message });
new Setting(contentEl)
.addButton(btn => btn
.setButtonText('Cancel')
.onClick(() => {
this.result = false;
this.close();
}))
.addButton(btn => btn
.setButtonText('Confirm')
.setCta()
.onClick(() => {
this.result = true;
this.close();
}));
}
onClose() {
this.onSubmit(this.result);
}
}
// Form input modal
export class InputModal extends Modal {
private value: string = '';
private onSubmit: (value: string | null) => void;
private placeholder: string;
private title: string;
constructor(
app: App,
title: string,
placeholder: string,
onSubmit: (value: string | null) => void
) {
super(app);
this.title = title;
this.placeholder = placeholder;
this.onSubmit = onSubmit;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: this.title });
new Setting(contentEl)
.setName('Input')
.addText(text => text
.setPlaceholder(this.placeholder)
.onChange(value => this.value = value));
new Setting(contentEl)
.addButton(btn => btn
.setButtonText('Cancel')
.onClick(() => {
this.close();
this.onSubmit(null);
}))
.addButton(btn => btn
.setButtonText('Submit')
.setCta()
.onClick(() => {
this.close();
this.onSubmit(this.value);
}));
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
```
### Step 2: Suggestion Popups (FuzzySuggestModal)
```typescript
import { App, FuzzySuggestModal, TFile } from 'obsidian';
// File picker modal
export class FileSuggestModal extends FuzzySuggestModal<TFile> {
private onSelect: (file: TFile) => void;
constructor(app: App, onSelect: (file: TFile) => void) {
super(app);
this.onSelect = onSelect;
}
getItems(): TFile[] {
return this.app.vault.getMarkdownFiles();
}
getItemText(file: TFile): string {
return file.path;
}
onChooseItem(file: TFile, evt: MouseEvent | KeyboardEvent): void {
this.onSelect(file);
}
}
// Generic suggestion modal
export class SuggestModal<T> extends FuzzySuggestModal<T> {
private items: T[];
private getText: (item: T) => string;
private onSelect: (item: T) => void;
constructor(
app: App,
items: T[],
getText: (item: T) => string,
onSelect: (item: T) => void
) {
super(app);
this.items = items;
this.getText = getText;
this.onSelect = onSelect;
}
getItems(): T[] {
return this.items;
}
getItemText(item: T): string {
return this.getText(item);
}
onChooseItem(item: T, evt: MouseEvent | KeyboardEvent): void {
this.onSelect(item);
}
}
// Usage:
new SuggestModal(
this.app,
['Option 1', 'Option 2', 'Option 3'],
(item) => item,
(selected) => console.log('Selected:', selected)
).open();
```
### Step 3: Custom Views (Leaf Views)
```typescript
import { ItemView, WorkspaceLeaf } from 'obsidian';
export const VIEW_TYPE_CUSTOM = 'custom-view';
export class CustomView extends ItemView {
constructor(leaf: WorkspaceLeaf) {
super(leaf);
}
getViewType(): string {
return VIEW_TYPE_CUSTOM;
}
getDisplayText(): string {
return 'Custom View';
}
getIcon(): string {
return 'dice';
}
async onOpen() {
const container = this.containerEl.children[1];
container.empty();
// Add content
container.createEl('h4', { text: 'Custom View Title' });
const content = container.createDiv({ cls: 'custom-view-content' });
content.createEl('p', { text: 'This is a custom view!' });
// Add interactive elements
const button = content.createEl('button', { text: 'Click me' });
button.addEventListener('click', () => {
console.log('Button clicked!');
});
}
async onClose() {
// Cleanup
}
}
// Register in main.ts:
export default class MyPlugin extends Plugin {
async onload() {
this.registerView(
VIEW_TYPE_CUSTOM,
(leaf) => new CustomView(leaf)
);
// Add ribbon icon to open view
this.addRibbonIcon('dice', 'Open Custom View', () => {
this.activateView();
});
// Add command
this.addCommand({
id: 'open-custom-view',
name: 'Open Custom View',
callback: () => this.activateView(),
});
}
async activateView() {
const { workspace } = this.app;
let leaf = workspace.getLeavesOfType(VIEW_TYPE_CUSTOM)[0];
if (!leaf) {
// Create new leaf in right sidebar
leaf = workspace.getRightLeaf(false);
await leaf.setViewState({
type: VIEW_TYPE_CUSTOM,
active: true,
});
}
workspace.revealLeaf(leaf);
}
onunload() {
// Clean up view
this.app.workspace.detachLeavesOfType(VIEW_TYPE_CUSTOM);
}
}
```
### Step 4: Editor Extensions (CodeMirror 6)
```typescript
import { EditorView, ViewPlugin, Decoration, DecorationSet } from '@codemirror/view';
import { RangeSetBuilder } from '@codemirror/state';
import { syntaxTree } from '@codemirror/language';
// Custom decoration plugin
const highlightPlugin = ViewPlugin.fromClass(class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: any) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter: (node) => {
if (node.name === 'HyperLink') {
builder.add(
node.from,
node.to,
Decoration.mark({ class: 'custom-highlight' })
);
}
},
});
}
return builder.finish();
}
}, {
decorations: (v) => v.decorations,
});
// Register in plugin:
this.registerEditorExtension(highlightPlugin);
```
### Step 5: Context Menus
```typescript
import { Menu, TFile, TFolder } from 'obsidian';
// Add to file menu
this.registerEvent(
this.app.workspace.on('file-menu', (menu: Menu, file: TAbstractFile) => {
if (file instanceof TFile && file.extension === 'md') {
menu.addItem((item) => {
item
.setTitle('My Custom Action')
.setIcon('star')
.onClick(async () => {
// Handle click
console.log('File:', file.path);
});
});
}
})
);
// Add to editor context menu
this.registerEvent(
this.app.workspace.on('editor-menu', (menu: Menu, editor: Editor, view: MarkdownView) => {
menu.addItem((item) => {
item
.setTitle('Insert Timestamp')
.setIcon('clock')
.onClick(() => {
editor.replaceSelection(new Date().toISOString());
});
});
})
);
```
## Output
- Modal dialogs for user input
- Suggestion popups with fuzzy search
- Custom sidebar views
- Editor decorations
- Context menus
## Error Handling
| Error | Cause | Solution |
|-------|-------|----------|
| View not showing | Not registered | Call `registerView` in `onload` |
| Modal closes immediately | Event propagation | Stop event propagation |
| Decorations not updating | Missing update handler | Implement `update` method |
| Menu item missing | Wrong event | Verify event type |
## Examples
### Progress Modal
```typescript
export class ProgressModal extends Modal {
private progressEl: HTMLElement;
private textEl: HTMLElement;
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: 'Processing...' });
this.textEl = contentEl.createEl('p', { text: 'Starting...' });
const progressContainer = contentEl.createDiv({ cls: 'progress-container' });
this.progressEl = progressContainer.createDiv({ cls: 'progress-bar' });
this.progressEl.style.width = '0%';
}
setProgress(percent: number, text?: string) {
this.progressEl.style.width = `${percent}%`;
if (text) this.textEl.setText(text);
}
onClose() {
this.contentEl.empty();
}
}
// Usage:
const modal = new ProgressModal(this.app);
modal.open();
for (let i = 0; i <= 100; i += 10) {
modal.setProgress(i, `Processing ${i}%`);
await sleep(100);
}
modal.close();
```
### Sidebar Styling
```css
/* styles.css */
.custom-view-content {
padding: 16px;
}
.custom-view-content h4 {
margin-bottom: 12px;
}
.custom-view-content button {
margin-top: 8px;
}
```
## Resources
- [Obsidian Modal Reference](https://docs.obsidian.md/Reference/TypeScript+API/Modal)
- [Obsidian View Reference](https://docs.obsidian.md/Reference/TypeScript+API/ItemView)
- [CodeMirror 6 Documentation](https://codemirror.net/docs/)
## Next Steps
For common errors, see `obsidian-common-errors`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!