Skip to main content

MarkdownEditor

Markdown

The WYSIWYG markdown surface; the Monaco SOURCE editor is Editor/CodeEditor and the read-only render is Editor/MarkdownPreview — see [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs).

import { MarkdownEditor } from "@elabs-ai/components-editor";
Install
npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-editor

Skipping the @source line is the most common first failure: Tailwind does not scan node_modules, so every component renders unstyled. The one-time wiring

Prompt for your coding agent

Paste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.

Add the brand-ui MarkdownEditor component (from @elabs-ai/components-editor) to this project.

1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`.
2. Look MarkdownEditor up before writing code: `npx -y @elabs-ai/components-cli docs MarkdownEditor` prints its real props, variants, what it pairs with and what to avoid.
3. If @elabs-ai/components-editor is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-editor` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-editor`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`.
4. Use it here: [the screen or file, and what it should show]
5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop.
6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.

Examples

11 live examples

Controlled

Read-only

Live directives (inline-editable)

The brand `:::` directives render as the REAL @brand components inside the editor (a live `Card`, `Alert`, `MetricBlock`), with the card/callout title and the metric label/value editable inline — not the old token-styled `toDOM` chrome. Switch the Storybook theme to confirm they track every theme.

Paste / drop image embed (onEmbedAsset)

Demonstrates `onEmbedAsset` — the paste/drop image-embed hook. The mock hook simulates a 600 ms upload delay and resolves with a data-URL so no real server is needed. To exercise it manually: 1. Copy an image file to your clipboard (or drag one onto the editor). 2. Paste / drop into the editor surface. 3. You will see the "Uploading…" chip appear, then the image render inline. 4. The markdown output below shows `![filename](/demo/uploads/...)`. The error variant is toggled by the "Simulate failure" button — it wires a hook that always rejects so you can observe the inline error chip + toast. Theme sweep: switch the Storybook theme (toolbar) to verify the upload chip and error chip use only semantic tokens across light/dark.

Editable table (wysiwyg-tables)

Demonstrates the table node-view feature: - Click inside a table cell → the **Table controls** toolbar appears at the bottom of the editor surface. - Toolbar buttons: Add row above/below, Delete row, Add col left/right, Delete column — all aria-labelled, keyboard-operable. - Tab moves to the next cell; Shift+Tab to the previous. - The serialized markdown (shown below the editor) stays lossless GFM. Cross-theme sweep: switch the Storybook theme toolbar to verify the table borders (`border-border-strong`), header tint (`bg-surface-muted`), and toolbar chrome use only semantic tokens across every theme.

Fills container (editor-fill-height)

Fill height (editor-fill-height): in a definite-height container, the editable must fill the whole pane — not just its content. A short (1-line) document still produces a full-height clickable surface, and the caret-anchored `/` menu has room to open near the bottom edge. Before the fix the host was content-sized (`min-height: 9rem` ≈ 144px), leaving a large dead area below the editable into which the slash menu clipped.

Code-block exit (Tab / Mod-Enter)

Code-block exit (A3): the caret can escape a fenced code block — including the trailing ```calc fence — with **Tab** or **Mod-Enter**. A code block that is the document's last child no longer traps the caret; exiting inserts a new paragraph below it. The behavior is unit-locked in `exit-keymap.test.ts`; this story is the manual playground — click inside the block and press Tab.

Iteration node menu (⋯ dropdown)

The iteration node-view's menu (#223): a `⋯` button opens "Edit iteration…" (the guided re-edit), "Change layout" (a submenu of radio items — stacked / grid / bento for `iterate` — that rewrites the `layout` attribute directly, no dialog), and "Convert to static" (replaces the directive with its populated markdown; no ProseMirror dependency). `IterationBuilderProvider` wires the `⋯`/right-click affordance on; without it neither menu renders. The `ContextMenu` story below opens the SAME item set via right-click — both read one shared list (`IterationMenuItems` in directive-views.tsx) so they can never diverge.

Iteration node menu (right-click)

The SAME iteration node menu opens on right-click — the AC's "never right-click-only" cuts both ways: the `⋯` button (story above) and the context menu (here) must expose IDENTICAL actions. Uses a `:::pivot` with real row/column values so "Transpose" (pivot-only) has data to swap.

Keyboard focus

#309 — the editable's own compound focus indicator. Resolves the governing theme element FROM THE SUBJECT (`el.closest("[data-theme]")`, never a guessed ancestor — quality-gates.md § Theme-safe), reads RESOLVED computed values (not the class string — a class-string assertion passed on the previous, invisible wrapper ring, which is exactly how this shipped), and asserts at least one of the indicator's two layers clears 3:1 against the ground the editable sits on. `markdown-editor.focus.test.ts` locks the stylesheet TEXT; this is the rendered-surface proof.

API

MarkdownEditor

PropTypeDescription
valuestringControlled markdown value. Pair with `onChange`.
defaultValuestringInitial markdown for uncontrolled use.
onChange(markdown: string) => voidFires on every edit with the full markdown document.
readOnlybooleanRender the editor read-only (still selectable, not editable).
ariaLabelstringAccessible name for the editable surface. Milkdown/ProseMirror renders an ARIA `textbox` (`contenteditable`); without a name, screen readers announce an unlabeled field. Set onto the ProseMirror view's `attributes`. Default `"Markdown editor"`.
slashMenuboolean | { commands?: SlashCommand[]; trigger?: string; shortcut?: string }Enable the `/` command menu (insert brand `:::` directives + basic blocks live at the caret). `true` (default) uses the built-in {@link BRAND_SLASH_COMMANDS}; pass a `commands` array to extend/replace them, or `false` to disable. The `trigger` defaults to `"/"`. Pass `shortcut` (default `"Mod-Shift-O"`) to control the keyboard shortcut that opens the menu at the caret without typing the trigger character — works in BOTH the WYSIWYG pane and the Monaco source pane (#271).
calcCalcEditorHooksOpt-in calc authoring inside ```calc fences (off by default). Supply the consumer's hooks — `tokenize` (highlight), `evaluate` (result inlays), and/or `complete` (autocomplete; Monaco surface). The library DECORATES, the consumer COMPUTES — no calc engine is bundled. Mirrors `MarkdownPreview`'s `evaluate`.
completionsEditorCompletionProvider[]Declarative completion providers (#283) — e.g. `[[wikilink]]` autocomplete. Off by default; mirrors the `slashMenu`/`calc` opt-in pattern. Each provider registers a `triggerCharacters` set + a `provide(ctx)` that returns candidates; the library owns detecting the trigger and rendering/inserting. This is a DELIBERATELY MINIMAL, best-effort mirror of the Monaco source-pane behavior (see `markdown-editor/completions/completions-prose.ts` for the exact gaps — no real cross-block line/column, a plain listbox instead of a native suggest widget). Forwarded from `MarkdownWorkspace`'s `completions` prop, which also wires the FULL Monaco/source-pane path.
onEmbedAssetEmbedAssetFnHost-provided callback for image paste/drop embedding. When set, the editor intercepts paste and drop events that contain image files, shows an inline "uploading…" placeholder, calls this function with the `File`, and on resolve inserts `![filename](returnedPath)` in the document. On reject the placeholder is removed and an inline error chip (+ toast) is shown — the document never contains a broken image. If not set, the editor's default paste/drop behavior is unchanged. The library NEVER stores assets — all persistence is the host's responsibility.

Also accepts: Omit< HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue" >