Skip to content
WANCRAFT

F.176Forms & Inputs

Markdown Editor

A textarea Markdown editor with a roving-focus toolbar, undoable formatting, shortcuts and a safe Write / Preview.

NewReactMotionTailwindadvanced

Overview

Markdown Editor keeps the humble textarea and makes it pleasant. A formatting toolbar (heading, bold, italic, quote, code, link, bulleted and numbered lists) acts on the current selection: inline styles wrap or unwrap it, block styles toggle the prefix on every selected line, and code turns multi-line selections into a fenced block. Edits go through the browser's editing pipeline, so Ctrl/Cmd+Z undoes a formatting step like any typed text, and Ctrl/Cmd+B, I and K work while typing. The textarea grows with its content between a minimum and maximum number of rows, a character count tracks an optional limit, and Write / Preview tabs show a rendering built from React elements: raw HTML is shown as text and only http, https and mailto links become links.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/markdown-editor.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/markdown-editor.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/markdown-editor.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { MarkdownEditor } from "@/components/ui/markdown-editor";
import { useState } from "react";

export function MarkdownEditorExample() {
  const [notes, setNotes] = useState("## Hello\n\nWrite **Markdown** here.");

  return (
    <div className="w-full max-w-xl">
      <MarkdownEditor
        label="Release notes"
        value={notes}
        onChange={setNotes}
        name="notes"
      />
    </div>
  );
}

03Props

PropTypeDefaultDescription
value / defaultValuestring— / ""Controlled Markdown, or the initial text when uncontrolled.
onChange(value: string) => void—Called on every edit, including toolbar formatting.
label / hintReactNode"Comment" / —Visible <label> for the textarea and helper text (aria-describedby).
placeholderstring"Write in Markdown…"Textarea placeholder.
maxLengthnumber0Character limit enforced by the textarea; 0 means no limit (the count still shows).
minRows / maxRowsnumber5 / 12The textarea grows from minRows and scrolls after maxRows.
showCountbooleantrueCharacter count (n or n/max) in the footer.
defaultTab"write" | "preview""write"Tab shown first.
accentstring"#FF7448"Tab indicator, focus rings, caret, link underline and bullets.
namestring—Name of the submitted textarea.
disabledbooleanfalseDisables the textarea and toolbar.
MarkdownPreview({ source, className? }) => JSX—Named export: the same safe renderer, for showing saved Markdown elsewhere.

04Features

  • 01Toolbar for heading, bold, italic, quote, code, link, bulleted and numbered lists, acting on the selection
  • 02Inline styles wrap or unwrap; line styles toggle per selected line and switch between list types; multi-line code becomes a fence
  • 03Formatting goes through execCommand("insertText") so Ctrl/Cmd+Z undoes it; setRangeText + an input event is the fallback
  • 04Ctrl/Cmd+B, I and K shortcuts inside the textarea; they stop propagating so page-level hotkeys such as a ⌘K palette don't also fire
  • 05Auto-growing textarea between min and max rows, with a character count and optional limit
  • 06Write / Preview tabs with a sliding indicator and a safe renderer built from React elements
motion · springmotion · layoutinput · keyboardinput · clickinput · focus

05Accessibility

  • The textarea has a real <label>; hint and character count are linked with aria-describedby.
  • Formatting buttons form a role="toolbar" (aria-controls the textarea) that is a single tab stop: Left / Right arrows move with wrap-around, Home / End jump to the ends; each button has a name and aria-keyshortcuts.
  • Write / Preview follow the tabs pattern: role="tablist", aria-selected, aria-controls, arrow keys and Home / End switch tabs; the preview panel is focusable.
  • Clicking a toolbar button keeps the textarea selection; focus returns to the textarea with the result selected.
  • Tab still moves focus out of the textarea (it is never captured for indentation).
  • prefers-reduced-motion makes the tab indicator move instantly.

06Limitations

  • The preview supports a subset only: ATX headings (#), **bold** / __bold__, *italic* / _italic_, `inline code`, fenced ``` code blocks, flat - / * / 1. lists, > blockquotes and [text](url) links. Nested lists, tables, images, setext headings, reference links and raw HTML are not rendered (HTML is shown as text).
  • Links are only rendered for http, https and mailto URLs; anything else keeps its text without a link.
  • Single line breaks are kept as <br> (comment-style), unlike CommonMark paragraphs.
  • execCommand is deprecated but is still the only way to keep native undo for programmatic edits; browsers without it fall back to setRangeText, which is not undoable.