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:

EngineResult
ChromiumPasses: a tool, native undo and redo; each tool is its own undo step.
WebKitPasses, with one difference: it merges consecutive edits into one undo step.
FirefoxNot 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.

FrameworkPackageUse
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-editora snippet and a plugin: x-data="lilyMarkdownEditor(options)"
Nunjucks@lilydesignsystem/nunjucks-markdown-editora macro, plus autoInit() in the browser
BlazorLilyDesignSystem.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.