Skip to content
Back to skills

Obsidian Core Workflow B

ASecurity

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".

  • 2,688 stars
  • 0 votes
  • 0 copies
  • 16 views
  • Added February 7, 2026
developmenttypescriptbashnodeapidocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill obsidian-core-workflow-b --agent claude-code

Installs 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 with every re-scan.

Security grade badge for Obsidian Core Workflow B
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/jeremylongshore-obsidian-core-workflow-b/badge)](https://www.skillsdirectory.com/skills/jeremylongshore-obsidian-core-workflow-b)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.