JSPM

@fedoup/markdown-editor

0.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 6
  • Score
    100M100P100Q64025F
  • License MIT

Obsidian-style Live Preview markdown editor for React, built on CodeMirror 6. One editor instance — rendered markdown by default, click any line to edit the raw source with no buffer swap.

Package Exports

  • @fedoup/markdown-editor
  • @fedoup/markdown-editor/styles.css

Readme

@fedoup/markdown-editor

Obsidian-style Live Preview markdown editor for React, built on CodeMirror 6.

One editor instance throughout. Rendered markdown by default. Click any line to edit the raw source for that line, with the cursor at your click position. Move away, the rendering returns. There is no preview/editor swap, no buffer, no lag — the editor is one CodeMirror view; toggling between "rendered" and "source" is a single decoration update.

npm install @fedoup/markdown-editor \
  @codemirror/commands @codemirror/lang-markdown @codemirror/language \
  @codemirror/state @codemirror/view @lezer/common
import { MarkdownEditor } from "@fedoup/markdown-editor";
import "@fedoup/markdown-editor/styles.css";

export function MyNote() {
  const [doc, setDoc] = useState("# Hello\n\nClick any line.");
  return <MarkdownEditor initialValue={doc} onChange={setDoc} />;
}

What you get

  • Headings h1–h6 with proportional sizing and stable line-height (no twitch on cursor-on/off).
  • Bold (**x**), italic (*x* / _x_), inline code (`x`) — syntax tokens hide off-line, reappear on-line.
  • Links[label](url) shows the styled label only; brackets and URL hide off-line.
  • Images![alt](url) renders as an inline <img> widget off-line, with a host-supplied imageResolver for path translation.
  • Fenced code — opening ```lang and closing ``` lines hide entirely when the cursor isn't in the block; reveal the moment it enters.
  • Lists — bullet and ordered list markers stay visible but muted.
  • Source mode — pass sourceMode={true} to disable Live Preview entirely (Obsidian's escape hatch).
  • State preservation — selection, scroll, undo history all survive alt-tab because the editor instance never unmounts. No "preview snaps back to editor" lag, no lost cursor.
  • Tiny — ~9 kB raw, ~3 kB gzipped (excludes CodeMirror, which you bring as peer deps).

How it works

This is not a WYSIWYG editor. The document model is plain markdown text — the editor literally holds **bold** as characters. What changes is what you see: a CodeMirror 6 ViewPlugin walks the Lezer markdown syntax tree on every doc, viewport, or selection change, and emits two kinds of decorations:

  1. Decoration.mark over the content (e.g. the word bold), adding CSS classes that style it.
  2. Decoration.replace over the syntax tokens (the **s), hiding them — but only on lines the cursor isn't currently on.

That single conditional is what makes the experience feel like Obsidian's Live Preview: the line the cursor lives on is always shown as raw source; every other line is shown as if it were rendered. Click moves the cursor, the decoration set rebuilds in the same frame, and the line you clicked unfolds. No component swap, no race.

The pattern is documented in the canonical discuss.codemirror.net thread. Obsidian's own implementation is closed source, but uses CodeMirror 6 and the same fundamental approach (their CM5→CM6 migration shipped Live Preview as its headline feature). kenforthewin/atomic-editor is a related open-source clone in the same family.

API

interface MarkdownEditorProps {
  initialValue: string;
  onChange?: (next: string) => void;
  sourceMode?: boolean;          // disable Live Preview decorations
  extraExtensions?: Extension[]; // add validation, paste handlers, line numbers, etc.
  placeholder?: string;
  className?: string;
  /**
   * Rewrite an image's `src` before the widget loads it. Useful for vault-
   * relative paths or auth-protected sources. Return `null`/`undefined` to
   * skip the widget for that image (alt + raw markdown fall back).
   */
  imageResolver?: (src: string) => string | null | undefined;
}

interface MarkdownEditorHandle {
  insertAtCursor: (text: string) => void;
  focus: () => void;
  getValue: () => string;
  view: EditorView | null;       // escape hatch for custom transactions
}

initialValue is read once. The editor is uncontrolled afterwards — listen via onChange and write back via the imperative handle if needed.

Custom extensions

import { MarkdownEditor } from "@fedoup/markdown-editor";
import { lineNumbers } from "@codemirror/view";
import { closeBrackets } from "@codemirror/autocomplete";

<MarkdownEditor
  initialValue={doc}
  onChange={setDoc}
  extraExtensions={[lineNumbers(), closeBrackets()]}
/>

Line numbers are not built in — they're an opt-in extension. Same goes for autocomplete, keymaps, paste handlers.

Theming

The editor reads design-token CSS custom properties. Any of these will be picked up:

Token Default fallback Purpose
--me-fg var(--foreground, currentColor) text + caret
--me-fg-muted var(--muted-foreground, #888) gutter, h6, list markers
--me-border var(--border, #ddd) gutter divider
--me-link var(--primary, #2563eb) link labels
--me-font-mono system monospace stack inline + fenced code
--me-font-prose system sans stack body
--me-font-size 14px base size

If you already use shadcn/ui or Tailwind tokens, you get the editor themed for free — the --foreground / --border / --muted-foreground / --primary fallbacks pull straight from the same tokens.

Try it locally

git clone https://github.com/fedoup/markdown-editor
cd markdown-editor
npm install
cd examples && npm install && npm run dev

Open the URL Vite prints. Click around. Alt-tab. Pop the Source mode checkbox.

Roadmap

  • v0.2 — sub-line activation (only hide tokens not directly under the cursor, instead of activating the whole line — matches Obsidian's behavior on long lines).
  • v0.2 — paste-as-markdown helper (incoming HTML → markdown, drop-anywhere image upload via host callback).
  • v0.2 — task-list checkbox widget (- [ ] / - [x] clickable).

What this isn't

  • Not a WYSIWYG editor. The model is markdown text. Round-tripping is byte-for-byte. If you want a model where the source is HTML/JSON and markdown is lossy I/O, look at Tiptap or Milkdown.
  • Not Obsidian. No graph view, no plugins, no vault layer. This is just the editing experience as a React component.

License

MIT