Live Preview Decorations
This demo includes Obsidian-style live preview — markdown syntax (**, *, ~~, #) is hidden when your cursor is on a different line, showing rendered formatting instead. Click on a line to reveal its raw markdown.
Markdown Source (Live Preview) 0 words · 0 chars · 0 lines
Rendered Preview

Editing Modes

Use the toggle above to switch between three modes:

  • Source — Raw markdown with syntax highlighting and live preview decorations
  • Split — Side-by-side source and rendered preview
  • Preview — Rendered HTML only

Live Preview Decorations (Obsidian-Style)

The key innovation: markdown syntax markers (**, *, ~~, #) are visually hidden when your cursor is on a different line, replaced with rendered formatting. Move your cursor to any line to reveal its raw markdown.

typescript
import { ViewPlugin, Decoration, WidgetType } from '@codemirror/view';

// Obsidian-style live preview: hide markdown syntax
// when cursor is NOT on the current line
const livePreviewPlugin = ViewPlugin.fromClass(class {
  decorations;
  constructor(view) {
    this.decorations = this.buildDecorations(view);
  }
  update(update) {
    if (update.docChanged || update.selectionSet) {
      this.decorations = this.buildDecorations(update.view);
    }
  }
  buildDecorations(view) {
    const widgets = [];
    const cursorLine = view.state.doc.lineAt(
      view.state.selection.main.head
    ).number;

    // For each line NOT containing the cursor:
    // - Find markdown syntax (**, *, ~~, #, etc.)
    // - Replace markers with Decoration.mark({ class: 'hidden' })
    // - Style the content (bold, italic, heading size, etc.)
    
    return Decoration.set(widgets);
  }
}, { decorations: v => v.decorations });

Custom Extensions

Everything in CM6 is an extension. Here's a word counter as a StateField:

typescript
import { StateField } from '@codemirror/state';

// Custom state field: word counter
const wordCountField = StateField.define<number>({
  create(state) {
    return countWords(state.doc.toString());
  },
  update(value, tr) {
    if (tr.docChanged) {
      return countWords(tr.state.doc.toString());
    }
    return value;
  }
});

// Use it: state.field(wordCountField) → number

Extension Types

State Fields

Custom state attached to the editor (e.g., word count, decoration state, fold state)

View Plugins

DOM-interacting logic (e.g., tooltip positioning, live preview decorations)

Facets

Configurable behaviors (e.g., tab size, line wrapping, read-only mode)

Keymaps

Key binding configurations. Compose multiple keymaps with priority ordering.

Decorations

Visual modifications without changing the document (marks, widgets, line decorations)

Themes

CSS-in-JS theming via EditorView.theme() — composable and overridable

Keyboard Shortcuts

ActionShortcut
UndoCtrl+Z
RedoCtrl+Shift+Z
Select allCtrl+A
IndentTab
Move line upAlt+
Move line downAlt+
Toggle commentCtrl+/

Strengths

  • Modular — import only what you need (~150KB)
  • Excellent performance (virtual rendering)
  • First-class TypeScript support
  • Mobile & accessibility support
  • Markdown-native with Lezer parser
  • Powers Obsidian — proven at scale
  • Active development by Marijn Haverbeke

Weaknesses

  • Steeper learning curve than simpler editors
  • No built-in WYSIWYG — requires decorations
  • Documentation is thorough but dense
  • No official Svelte wrapper (easy to make)