Installation
Theme
Mode

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-editor

Overview

Live — select text and hit a toolbar button or ⌘B / ⌘I / ⌘K, press Enter inside a list to continue it, then switch to Preview.

Styled with Markdown · B I K66 words

Capabilities

Markdown is the source of truth; the toolbar and shortcuts are conveniences over it, and Preview shows the result.

Toolbar & shortcuts

Bold, italic, headings, lists, checklists, links and code — one click each, or ⌘B / ⌘I / ⌘K. They wrap the current selection.

Three views, one value

Write - Markdown is the raw source, Write - Text formats as you type, Preview renders the result — all over the same Markdown string.

Rich text for everyone

The Write - Text tab hides the syntax: click Bold and type, and it's bold on screen. Underneath it still writes portable Markdown.

Custom tabs

modes trims or reorders the tabs and modeLabels renames them — down to a single Write tab for non-technical audiences.

Smart lists

Press Enter in a list and the next bullet, number or checkbox is created for you; an empty item ends the list.

Plain-text underneath

The value is portable Markdown — store, diff and render it anywhere, no proprietary document format.

Slash commands

Opt in to the Slash Menu: type / for a caret-anchored list of blocks — headings, lists, quotes, code — or bring your own command set.

Read-only preview

Hide the toolbar while previewing, or lock the mode from outside for a clean rendered document viewers can't edit.

Syntax highlighting

Fence a code block with a language tag (```ts, ```css, ```bash…) and Preview tokenizes it — no highlighter dependency, colors from theme tokens.

@ Mentions

Pass a people list and @ opens a typeahead; @handles render as mention chips in Preview either way.

Tables

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.

Images

Drag-drop, paste, or upload — the file becomes ![alt](url) 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.

Rich text — saved as Markdown · B I K66 words
Rich text — saved as Markdown · B I K23 words

Compact

Drop minHeight for a comment-sized input that still carries the full toolbar.

Styled with Markdown · B I K0 words

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.

Styled with Markdown · B I K /0 words

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).

Release script
// Tag and publish the current build
export async function release(tag: string) {
  const notes = await collectNotes(tag);
  if (!notes.length) throw new Error("empty changelog");
  return publish({ tag, notes, draft: false });
}
# Verify locally first
npm run build --workspaces
git tag v1.2.0 && git push --tags
Styled with Markdown · B I K54 words

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).

Styled with Markdown · B I K11 words

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.

Styled with Markdown · B I K57 words

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 ![alt](url) 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.

Drop an image anywhere on the editor — or paste one, or use the toolbar's image button.

Orbit banner

The file becomes ![alt](url) in the Markdown; Preview renders it inline.

Styled with Markdown · B I K30 words

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.

60 chars
Styled with Markdown · B I K12 words

Hidden toolbar & read-only

hideToolbarInPreview drops the formatting tools while previewing — the Write / Preview tabs stay so the reader can flip back. Lock the mode from outside (controlled mode with no onModeChange) and the whole bar goes too: a read-only rendered document for viewers who can't edit.

Onboarding flow v2

Rework the first-run experience so a new workspace reaches its first created task in under two minutes.

Scope
Audit current empty states
Rebuild the sample project
Add the checklist nudge

See the design spec for the full flow. Track progress with orbit status.

Keep copy short — people skim the first run.
Styled with Markdown66 words
Onboarding flow v2

Rework the first-run experience so a new workspace reaches its first created task in under two minutes.

Scope
Audit current empty states
Rebuild the sample project
Add the checklist nudge

See the design spec for the full flow. Track progress with orbit status.

Keep copy short — people skim the first run.
Styled with Markdown66 words

Props

PropTypeDefaultDescription
valuestringControlled Markdown value. Pair with onValueChange.
defaultValuestring""Initial Markdown when uncontrolled.
onValueChange(value: string) => voidCalled 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) => voidCalled when the mode tabs switch.
modesMarkdownEditorMode[]["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.
modeLabelsPartial<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.
minHeightnumber260Minimum pane height in px (spec range 140–520). The textarea stays user-resizable.
placeholderstring"Write a description… Markdown supported."Textarea placeholder.
ariaLabelstring"Markdown editor"Accessible name for the textarea.
slashCommandsboolean | 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) => voidCalled after a slash command inserts and the menu closes.
mentionsMarkdownEditorMention[]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) => voidCalled 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 ![alt](url) — 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.
hideToolbarInPreviewbooleanfalseHide 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.
…restHTMLAttributes<HTMLDivElement>Everything else is forwarded to the root frame, including ref.
PropTypeDefaultDescription
MARKDOWN_SLASH_COMMANDSSlashCommandGroup[]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) => ReactNodeThe 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) => ReactNodeThe 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

Do

Show the rendered result

Let people preview before they post. Seeing the formatting prevents surprised “why is this all bold” moments.

Don’t

Hide the source by default

Keep Write - Markdown offered unless the audience truly never needs it. The value is Markdown people may meet elsewhere.

Do

Support keyboard shortcuts

⌘B / ⌘I / ⌘K match every other editor. Honoring them makes the field feel native, not like a toy.

Don’t

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.

⌘ / Ctrl + BBold the selection
⌘ / Ctrl + IItalicize the selection
⌘ / Ctrl + KInsert a link — a small URL dialog in Write - Text
EnterContinue the current list — or insert the highlighted slash command
/Open the slash command menu (Write - Markdown, when slashCommands is set)
@Open the mention typeahead (Write - Markdown, when mentions is set)
↑ ↓Move the open menu's highlight
EscDismiss the open menu