Live playground
Serialized markdown (live)
# Try editing this
Every edit round-trips through **barkdown**'s codec — *italic*, ~~strikethrough~~, `inline code`, and a [link](https://github.com/kevinpeckham/woof-editor) all serialize back to the exact markdown a parser would produce.
Select text for the bubble menu, right-click for the context menu, or click the gutter chip to the left of any block to change its type.
## Two kinds of lists
- Bulleted items work as expected
- So does **emphasis** inside a bullet
- Undo, redo, and save are wired to the toolbar above
1. Numbered lists too
2. Keyboard shortcuts are listed below the editor
> Round-trip means the markdown you started with and the markdown you get back after editing are structurally identical — no drift, no diff noise.
```ts
const editor = new MarkdownEditorState({ markdown: "# Hello" });
```
This editor understands footnotes, too[^1].
[^1]: Click the footnote number to edit its definition inline.
This page styles the document via the class prop; menus are themed with two
CSS variables.
Why another markdown WYSIWYG
Most contenteditable markdown editors round-trip lossily: markdown renders to DOM, you edit the DOM, and a separate serializer writes markdown back out through a different path than the one that read it in. Content drifts a little on every save — extra whitespace, quote-style changes, list markers that flip.
woof-editor serializes through @kevinpeckham/barkdown instead, whose DOM → markdown writer is property-tested to invert marked's parser: toMarkdown(toDom(md)) === md for every canonical input.
That guarantee is what lets you store editor output as canonical markdown in a database or a git repo — CMS bodies that survive edit → save → reload cycles without accumulating cruft.
Usage
<script lang="ts">
import { MarkdownEditor, MarkdownEditorState } from "@kevinpeckham/woof-editor";
const editor = new MarkdownEditorState({
markdown: "# Hello\n\nEditable markdown, round-tripped through barkdown.",
});
async function save() {
await fetch("/api/articles/123", {
method: "PUT",
body: JSON.stringify({ markdown: editor.markdownCurrent }),
});
editor.markAsSaved(); // flips `hasEdits` to false without re-seeding the DOM
}
</script>
<MarkdownEditor {editor} />
<button disabled={!editor.canUndo} onclick={() => editor.undo()}>Undo</button>
<button disabled={!editor.canRedo} onclick={() => editor.redo()}>Redo</button>
<button disabled={!editor.hasEdits} onclick={save}>Save</button>