Markdown editor
A <textarea> with a top bar of buttons for rich-text effects. The buttons write Markdown into the
field, so the value is plain text: it submits with a form, validates, and works without JavaScript.
What it is, and what it is not
Nine tools: heading, bold, italic, strikethrough, code, link, quote, bulleted list and numbered list. Press a tool over text that already has the formatting and it comes off. The formatted text stays selected, so effects stack, and focus returns to the field. Ctrl/Cmd + B, I and K apply bold, italic and link.
It is not a WYSIWYG editor: it does not draw bold text in the field, edit a document tree, or parse or sanitise Markdown. Render the value with your own Markdown library, and sanitise the HTML you produce. It ships no CSS, no Markdown parser and no English: the names of the field, the toolbar and every tool are props, and a tool with no name is not shown.
Quick start (Svelte)
<script>
import MarkdownEditor from "@lilydesignsystem/svelte-markdown-editor";
let body = $state("");
const labels = {
heading: "Heading", bold: "Bold", italic: "Italic", strikethrough: "Strikethrough", code: "Code",
link: "Link", quote: "Quote", bulletList: "Bulleted list", numberedList: "Numbered list",
};
</script>
<MarkdownEditor
bind:value={body}
label="Message"
toolbarLabel="Formatting"
{labels}
name="body"
rows={8}
/> tools chooses and orders the buttons; headingLevel sets how many # the
heading tool writes; icon replaces a tool's icon.
Accessibility
- A native
<textarea>and native<button type="button">s: form submission,required, spellcheck and IME all come from the platform. role="toolbar"with a roving tabindex: one tab stop, ← → move between buttons, Home and End jump.- Icons are decorative; each button's name comes from
labels. - The result is Markdown, so a screen reader hears
**bold**as typed. Offer a rendered preview if your readers will not read Markdown.
Undo and redo, checked in real browsers
Edits go through execCommand("insertText"), so native undo and redo keep working. The Svelte package
runs the same Playwright spec in each engine:
| Engine | Result |
|---|---|
| Chromium | Passes: a tool, native undo and redo; each tool is its own undo step. |
| WebKit | Passes, with one difference: it merges consecutive edits into one undo step. |
| Firefox | Not run: Playwright's Firefox 157 does not launch on the maintainer's machine. |
Every framework
Svelte is canonical; the other packages follow it, and the formatting logic is one shared module copied into each TypeScript or JavaScript package.
| Framework | Package | Use |
|---|---|---|
| Svelte | @lilydesignsystem/svelte-markdown-editor | <MarkdownEditor bind:value …> |
| React | @lilydesignsystem/react-markdown-editor | <MarkdownEditor value onChange …> |
| Vue | @lilydesignsystem/vue-markdown-editor | <MarkdownEditor v-model …> |
| Angular | @lilydesignsystem/angular-markdown-editor | <lily-markdown-editor [(value)] …> |
| HTML | @lilydesignsystem/html-markdown-editor | <markdown-editor>, a custom element |
| Web Components | @lilydesignsystem/web-components-markdown-editor | <lily-markdown-editor> |
| Alpine.js | @lilydesignsystem/alpine-markdown-editor | a snippet and a plugin: x-data="lilyMarkdownEditor(options)" |
| Nunjucks | @lilydesignsystem/nunjucks-markdown-editor | a macro, plus autoInit() in the browser |
| Blazor | LilyDesignSystem.Blazor.MarkdownEditor (NuGet) | <MarkdownEditor @bind-Value …> |
The Blazor port is a C# translation of the formatting logic and cannot decide preventDefault per key, so
it has no keyboard shortcuts, and Home/End on its toolbar also scroll the page.
Not published yet. The packages are in the monorepo; none is on npm or NuGet yet.