Skip to main content
Obsidian provides a rich set of UI components for building plugin interfaces. This guide covers the essential components for creating modals, notices, and settings. Modals are popup dialogs that appear over the main interface. They’re perfect for forms, confirmations, and focused interactions.

Basic Modal

Extend the Modal class to create custom modals:
import { App, Modal } from 'obsidian';

export class ExampleModal extends Modal {
  constructor(app: App) {
    super(app);
  }

  onOpen() {
    const { contentEl } = this;

    contentEl.createEl('h2', { text: 'Example Modal' });
    contentEl.createEl('p', { text: 'This is a modal dialog.' });

    const button = contentEl.createEl('button', { text: 'Close' });
    button.addEventListener('click', () => {
      this.close();
    });
  }

  onClose() {
    const { contentEl } = this;
    contentEl.empty();
  }
}

// Open the modal
const modal = new ExampleModal(this.app);
modal.open();
Set the modal title using setTitle():
export class TitledModal extends Modal {
  onOpen() {
    this.setTitle('Settings');

    this.contentEl.createEl('p', {
      text: 'Configure your settings here.'
    });
  }
}
PropertyTypeDescription
appAppReference to the app instance
scopeScopeScope for registering hotkeys
containerElHTMLElementThe modal’s container element
modalElHTMLElementThe modal content area
titleElHTMLElementThe title element
contentElHTMLElementThe main content element
Create modals that return values:
export class InputModal extends Modal {
  result: string;
  onSubmit: (result: string) => void;

  constructor(app: App, onSubmit: (result: string) => void) {
    super(app);
    this.onSubmit = onSubmit;
  }

  onOpen() {
    const { contentEl } = this;

    contentEl.createEl('h3', { text: 'Enter a value' });

    const input = contentEl.createEl('input', {
      type: 'text',
      placeholder: 'Type here...'
    });
    input.addEventListener('change', (e) => {
      this.result = (e.target as HTMLInputElement).value;
    });

    const btnContainer = contentEl.createDiv();
    const submitBtn = btnContainer.createEl('button', { text: 'Submit' });
    submitBtn.addEventListener('click', () => {
      this.close();
      this.onSubmit(this.result);
    });
  }
}

// Usage
new InputModal(this.app, (result) => {
  console.log('User entered:', result);
}).open();
Use setCloseCallback() to handle cleanup when the modal closes.

SuggestModal

SuggestModal provides a searchable list of suggestions:
import { App, SuggestModal, TFile } from 'obsidian';

export class FileSuggestModal extends SuggestModal<TFile> {
  getSuggestions(query: string): TFile[] {
    return this.app.vault.getMarkdownFiles()
      .filter(file => 
        file.basename.toLowerCase().includes(query.toLowerCase())
      );
  }

  renderSuggestion(file: TFile, el: HTMLElement) {
    el.createEl('div', { text: file.basename });
    el.createEl('small', { text: file.path });
  }

  onChooseSuggestion(file: TFile, evt: MouseEvent | KeyboardEvent) {
    console.log('Selected:', file.path);
  }
}

FuzzySuggestModal

For fuzzy search with match highlighting:
import { FuzzySuggestModal } from 'obsidian';

interface Command {
  id: string;
  name: string;
}

export class CommandModal extends FuzzySuggestModal<Command> {
  getItems(): Command[] {
    return [
      { id: 'cmd1', name: 'Open settings' },
      { id: 'cmd2', name: 'Create note' },
      { id: 'cmd3', name: 'Toggle sidebar' }
    ];
  }

  getItemText(item: Command): string {
    return item.name;
  }

  onChooseItem(item: Command, evt: MouseEvent | KeyboardEvent) {
    console.log('Chose:', item.name);
  }
}

Notice Component

Notices are temporary notifications that appear at the top of the screen.

Basic Notice

import { Notice } from 'obsidian';

// Simple notice (default 5 seconds)
new Notice('Operation completed!');

// Custom duration (10 seconds)
new Notice('This stays longer', 10000);

// Permanent notice (until dismissed)
const notice = new Notice('Click to dismiss', 0);

Notice with Custom Content

const notice = new Notice('', 5000);
notice.noticeEl.empty();

const container = notice.noticeEl.createDiv();
container.createEl('strong', { text: 'Success!' });
container.createEl('br');
container.createEl('span', { text: 'Your file has been saved.' });

Update Notice Content

const notice = new Notice('Processing...', 0);

setTimeout(() => {
  notice.setMessage('Still processing...');
}, 2000);

setTimeout(() => {
  notice.setMessage('Complete!');
  setTimeout(() => notice.hide(), 1000);
}, 5000);
Set duration to 0 to keep the notice visible until manually dismissed.

Setting Component

The Setting class creates consistent setting rows:

Basic Setting

import { Setting } from 'obsidian';

const containerEl = document.createElement('div');

new Setting(containerEl)
  .setName('Setting name')
  .setDesc('Description of what this setting does')
  .addText(text => text
    .setPlaceholder('Enter value')
    .setValue('default')
    .onChange(async (value) => {
      console.log('New value:', value);
    }));

Toggle Setting

new Setting(containerEl)
  .setName('Enable feature')
  .setDesc('Turn this feature on or off')
  .addToggle(toggle => toggle
    .setValue(true)
    .onChange(async (value) => {
      console.log('Toggled:', value);
    }));
new Setting(containerEl)
  .setName('Choose option')
  .addDropdown(dropdown => dropdown
    .addOption('option1', 'Option 1')
    .addOption('option2', 'Option 2')
    .addOption('option3', 'Option 3')
    .setValue('option1')
    .onChange(async (value) => {
      console.log('Selected:', value);
    }));

Slider Setting

new Setting(containerEl)
  .setName('Adjust value')
  .setDesc('Use slider to set a value')
  .addSlider(slider => slider
    .setLimits(0, 100, 5)
    .setValue(50)
    .setDynamicTooltip()
    .onChange(async (value) => {
      console.log('Value:', value);
    }));

Button Setting

new Setting(containerEl)
  .setName('Dangerous action')
  .setDesc('This cannot be undone')
  .addButton(button => button
    .setButtonText('Delete')
    .setWarning()
    .onClick(async () => {
      console.log('Button clicked');
    }));

Multiple Components

new Setting(containerEl)
  .setName('Multiple inputs')
  .addText(text => text.setPlaceholder('First'))
  .addText(text => text.setPlaceholder('Second'))
  .addButton(button => button
    .setButtonText('Apply')
    .setCta());

PluginSettingTab

Create a settings tab for your plugin:
import { App, PluginSettingTab, Setting } from 'obsidian';
import ExamplePlugin from './main';

export class ExampleSettingTab extends PluginSettingTab {
  plugin: ExamplePlugin;

  constructor(app: App, plugin: ExamplePlugin) {
    super(app, plugin);
    this.plugin = plugin;
  }

  display(): void {
    const { containerEl } = this;
    containerEl.empty();

    containerEl.createEl('h2', { text: 'Settings' });

    new Setting(containerEl)
      .setName('API Key')
      .setDesc('Enter your API key')
      .addText(text => text
        .setPlaceholder('Enter key')
        .setValue(this.plugin.settings.apiKey)
        .onChange(async (value) => {
          this.plugin.settings.apiKey = value;
          await this.plugin.saveSettings();
        }));

    new Setting(containerEl)
      .setName('Auto-save')
      .setDesc('Automatically save changes')
      .addToggle(toggle => toggle
        .setValue(this.plugin.settings.autoSave)
        .onChange(async (value) => {
          this.plugin.settings.autoSave = value;
          await this.plugin.saveSettings();
        }));
  }
}

Register Settings Tab

export default class ExamplePlugin extends Plugin {
  settings: ExampleSettings;

  async onload() {
    await this.loadSettings();
    this.addSettingTab(new ExampleSettingTab(this.app, this));
  }

  async loadSettings() {
    this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
  }

  async saveSettings() {
    await this.saveData(this.settings);
  }
}

Color Picker

import { ColorComponent } from 'obsidian';

new Setting(containerEl)
  .setName('Theme color')
  .addColorPicker(color => color
    .setValue('#ff0000')
    .onChange(async (value) => {
      console.log('Color:', value);
    }));

Progress Bar

import { ProgressBarComponent } from 'obsidian';

const setting = new Setting(containerEl)
  .setName('Progress');

setting.controlEl.createDiv('', (el) => {
  const progress = new ProgressBarComponent(el);
  progress.setValue(50); // 0-100
});
Create context menus:
import { Menu } from 'obsidian';

const menu = new Menu();

menu.addItem((item) =>
  item
    .setTitle('Copy')
    .setIcon('copy')
    .onClick(() => {
      console.log('Copy clicked');
    })
);

menu.addItem((item) =>
  item
    .setTitle('Delete')
    .setIcon('trash')
    .setWarning(true)
    .onClick(() => {
      console.log('Delete clicked');
    })
);

menu.addSeparator();

menu.addItem((item) =>
  item
    .setTitle('Cancel')
    .onClick(() => {
      menu.hide();
    })
);

// Show at mouse event
menu.showAtMouseEvent(event);

Best Practices

1
Clean Up Resources
2
Always clean up in onClose():
3
onClose() {
  const { contentEl } = this;
  contentEl.empty();
}
4
Use Semantic HTML
5
Structure content properly:
6
const section = contentEl.createDiv({ cls: 'setting-section' });
section.createEl('h3', { text: 'Section Title' });
const form = section.createEl('form');
// Add form fields
7
Handle Keyboard Shortcuts
8
Register shortcuts in modals:
9
onOpen() {
  this.scope.register(['Ctrl'], 'Enter', () => {
    this.submit();
    return false;
  });

  this.scope.register([], 'Escape', () => {
    this.close();
    return false;
  });
}

Settings and Commands

Learn about commands and settings

Creating Views

Build custom view interfaces

Build docs developers (and LLMs) love