Forms & input
Markdown editor
A rich-text field for task descriptions and docs that writes plain Markdown underneath. Author in raw Markdown or in a rich-text view where formatting applies as you type, with a formatting toolbar, keyboard shortcuts, smart list continuation, slash commands, @ mentions, tables and image uploads — and a live Preview that renders headings, lists, checkboxes, quotes, links, tables, images and syntax-highlighted code.
npx shadcn@latest add @orbit/markdown-editorOverview
Live — select text and hit a toolbar button or ⌘B / ⌘I / ⌘K, press Enter inside a list to continue it, then switch to Preview.
Capabilities
Markdown is the source of truth; the toolbar and shortcuts are conveniences over it, and Preview shows the result.
Bold, italic, headings, lists, checklists, links and code — one click each, or ⌘B / ⌘I / ⌘K. They wrap the current selection.
Write - Markdown is the raw source, Write - Text formats as you type, Preview renders the result — all over the same Markdown string.
The Write - Text tab hides the syntax: click Bold and type, and it's bold on screen. Underneath it still writes portable Markdown.
modes trims or reorders the tabs and modeLabels renames them — down to a single Write tab for non-technical audiences.
Press Enter in a list and the next bullet, number or checkbox is created for you; an empty item ends the list.
The value is portable Markdown — store, diff and render it anywhere, no proprietary document format.
Opt in to the Slash Menu: type / for a caret-anchored list of blocks — headings, lists, quotes, code — or bring your own command set.
Hide the toolbar while previewing, or lock the mode from outside for a clean rendered document viewers can't edit.
Fence a code block with a language tag (```ts, ```css, ```bash…) and Preview tokenizes it — no highlighter dependency, colors from theme tokens.
Pass a people list and @ opens a typeahead; @handles render as mention chips in Preview either way.
Insert a pipe table from the toolbar's size grid, then align columns and add or delete rows and columns with the caret in place.
Drag-drop, paste, or upload — the file becomes  via your upload handler (or an inline data URL) and renders in Preview.
Write modes
Two editing surfaces over the same Markdown value. Write - Markdown is the raw source; Write - Text is a rich-text surface for people who don't know the syntax — click Bold and type, and it's bold immediately, with the DOM serialized back to Markdown on every input. modes picks which tabs are offered (and their order; the first entry is the default view unless defaultMode says otherwise), and modeLabels renames any tab. When only one write mode is offered, its tab is labeled just Write.
Compact
Drop minHeight for a comment-sized input that still carries the full toolbar.
Slash commands
slashCommands wires the Slash Menu component to the textarea — type / at the start of a line (or after a space) for a caret-anchored command list. Built-in commands map straight onto the toolbar's Markdown edits; ↑↓ move, Enter inserts, Esc dismisses. Pass your own SlashCommandGroup[] to customize the set.
Code highlighting
Name a language on the fence and Preview tokenizes the block — comments, strings, keywords, numbers, functions and types, colored with theme tokens so they hold up in dark mode and every preset. Built in for js / ts / jsx / tsx, json, css / scss, html / xml, python, and bash / shell; unknown languages render plain. The tokenizer is also exported as highlightCode(code, lang).
Mentions
Pass mentions and typing @ opens a people typeahead in the same caret-anchored popup as slash commands — filtered by name or username, ↑↓ / Enter / Esc, focus stays in the field. Picking inserts @username into the Markdown, and Preview renders any @handle as a mention chip (typed by hand or picked).
Tables
The toolbar's table menu inserts a pipe table from a hover size grid. Click inside any table and reopen the menu to edit it in place: align the current column left / center / right (the separator row's colons), or add and delete rows and columns. The menu stays open across actions, so several column styles can be applied in one visit — and Preview renders the alignment. /table works from the slash menu too.
Images
Drag an image onto the editor, paste one from the clipboard, or pick a file with the toolbar's image button — it lands in the Markdown as  and Preview renders it inline. By default the URL is an inline data URL (self-contained, no server); pass onImageUpload to upload the file and embed the hosted URL instead. An “Uploading…” placeholder holds the spot while the promise resolves and is removed on failure.
Controlled
value / onValueChange and mode / onModeChange make both the Markdown and the mode tabs fully controllable — drive any of the three views from outside or persist drafts as the user types.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Controlled Markdown value. Pair with onValueChange. |
| defaultValue | string | "" | Initial Markdown when uncontrolled. |
| onValueChange | (value: string) => void | — | Called with the next Markdown on every edit — typing, toolbar, shortcuts, list continuation. |
| mode | "write" | "text" | "preview" | — | Controlled editor mode — write is the Markdown source, text the rich-text surface, preview the render. Pair with onModeChange. |
| defaultMode | "write" | "text" | "preview" | modes[0] | Initial mode when uncontrolled. Defaults to the first entry of modes. |
| onModeChange | (mode) => void | — | Called when the mode tabs switch. |
| modes | MarkdownEditorMode[] | ["write", "text", "preview"] | Which mode tabs are offered, in order — trim it per audience, e.g. ["text", "preview"] for people who shouldn't meet Markdown syntax. With a single write mode in the list, its tab is labeled just Write. |
| modeLabels | Partial<Record<MarkdownEditorMode, ReactNode>> | — | Override any tab's label, e.g. { text: "Compose" }. Defaults: Write - Markdown / Write - Text / Preview, collapsing to Write when only one write mode is offered. |
| minHeight | number | 260 | Minimum pane height in px (spec range 140–520). The textarea stays user-resizable. |
| placeholder | string | "Write a description… Markdown supported." | Textarea placeholder. |
| ariaLabel | string | "Markdown editor" | Accessible name for the textarea. |
| slashCommands | boolean | SlashCommandGroup[] | — | Enable the inline / command menu — true for the built-in Markdown set (MARKDOWN_SLASH_COMMANDS), or your own groups. Commands whose id names a toolbar action (h1, h2, quote, bullet, numbered, check, link, code, codeblock) run its selection-aware edit; others insert their text (or chip label) at the caret. |
| onSlashCommand | (command: SlashCommand) => void | — | Called after a slash command inserts and the menu closes. |
| mentions | MarkdownEditorMention[] | — | People for the @ typeahead: { username, name?, description? }. Typing @ opens a caret-anchored list filtered by name / username; picking inserts @username. Preview chips any @handle with or without this list. |
| onMentionSelect | (user: MarkdownEditorMention) => void | — | Called after a mention inserts and the menu closes — e.g. to send a notification. |
| onImageUpload | (file: File) => string | Promise<string> | — | Turn a dropped / pasted / picked image into the URL embedded as  — upload it and return the hosted URL. Defaults to an inline data URL. An Uploading… placeholder holds the spot while pending; it's removed if the promise rejects. |
| hideToolbarInPreview | boolean | false | Hide the formatting toolbar while previewing. If the mode is also locked from outside — controlled mode with no onModeChange — the whole bar, tabs included, is removed for a read-only render. |
| …rest | HTMLAttributes<HTMLDivElement> | — | Everything else is forwarded to the root frame, including ref. |
| Prop | Type | Default | Description |
|---|---|---|---|
| MARKDOWN_SLASH_COMMANDS | SlashCommandGroup[] | — | The built-in slash command set — Blocks (headings, quote, divider), Lists (bulleted, numbered, checklist) and Insert (link, inline code, code block) — exported so you can extend or reorder it before passing it back via slashCommands. |
| renderMarkdown(md) | (md: string) => ReactNode | — | The preview renderer as a standalone pure function — headings, paragraphs, fenced code (highlighted when the fence names a language), quotes, hr, pipe tables with column alignment, bullet / numbered lists, checklists, @mention chips, inline images, and inline bold / italic / strike / code / links. It emits only known React elements from parsed text (no raw HTML), so output is sanitized by construction. |
| highlightCode(code, lang) | (code: string, lang?: string) => ReactNode | — | The fenced-block tokenizer on its own — a small regex highlighter (comments, strings, keywords, numbers, functions, types) for js / ts, json, css, html, python and bash, colored with theme tokens. Unknown languages return the plain string. |
Usage
Show the rendered result
Let people preview before they post. Seeing the formatting prevents surprised “why is this all bold” moments.
Hide the source by default
Keep Write - Markdown offered unless the audience truly never needs it. The value is Markdown people may meet elsewhere.
Support keyboard shortcuts
⌘B / ⌘I / ⌘K match every other editor. Honoring them makes the field feel native, not like a toy.
Over-format the toolbar
Offer the common marks, not every possible one. A wall of buttons is slower to scan than a focused set.
Keyboard & accessibility
The mode switch is a tablist (arrow keys cycle the offered tabs). Toolbar buttons carry aria-labels, aria-keyshortcuts and native tooltips, and disable while previewing. Shortcuts map to Ctrl on Windows / Linux automatically, in both write modes. The rich-text surface is a labeled multiline textbox; its link dialog opens on ⌘K, applies on Enter and dismisses on Esc. The slash and mention menus are listboxes the textarea controls via aria-activedescendant — focus never leaves the field. The preview emits semantic elements — lists, blockquotes, code — from parsed text only.