Markdown Editor

A real textarea, a toolbar that edits the selection through setRangeText — so the browser undo stack survives — and the Markdown component as its preview.

src/components/ui/markdown-editor.tsx

About

From the component's own source, where it was written.

A markdown editor: a real textarea, a toolbar that edits the selection, and the Markdown renderer as its preview.

**The input is a plain <textarea>.** Rich contenteditable editors have to reimplement undo, IME composition, spellcheck, autocorrect, mobile keyboards and every accessibility affordance a textarea gets from the platform — and they get the undo stack wrong first. Here the toolbar goes through setRangeText, which means **the browser's own undo history stays intact**: ctrl-Z after clicking Bold does what you expect, which is not true of an editor that rewrites value from React state.

**Preview is the same component that renders the document elsewhere.** An editor whose preview is a different renderer teaches you a formatting model your readers will not see, so what you check in the split view is exactly what Markdown produces — including where it stops.

Wrapping is selection-aware in the way people expect: with text selected the markers go around it, with nothing selected they are inserted and the caret lands **between** them, and applying the same marker to an already-wrapped selection unwraps it.

Install

npx astralyx-ui add markdown-editor

Usage

import { MarkdownEditor } from '@/components/ui/markdown-editor'

<MarkdownEditor value={text} onChange={setText} defaultMode="split" />

Composer

Change the props and the source below follows.

Pull request

Adds a QR encoder so TwoFactorSetup can fill its own qr slot.

  • Byte mode, versions 1-10
  • All eight masks scored, per the spec
  • Verified against an independent decoder
Nothing leaves the page.
Props
tsx
<MarkdownEditor
  value={text}
  onChange={setText}
  defaultMode="split"
/>

Examples

Write and preview together

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

MarkdownEditor

Extends Omit<ComponentProps<'div'>, 'onChange' | 'defaultValue'>.

PropTypeDefaultDescription
valuestring—The markdown source. Controlled or uncontrolled.
defaultValuestring''—
onChange(value: string) => void—The markdown source. Controlled or uncontrolled.
placeholderstring'Write markdown…'—
rowsnumber12Starting rows. Also the minimum height when autoResize is on.
autoResizebooleantrueGrow the textarea with its content, up to maxRows.
maxRowsnumber30Ceiling for autoResize. Past this the pane scrolls.
syncScrollbooleantrueKeep the two panes aligned while scrolling in split mode.
defaultModeMode'write'—
modesMode[]['write', 'preview', 'split']Drop 'split' on narrow screens by passing your own list.
toolbarbooleantrue—
actionsReactNode—Trailing slot in the toolbar — a save button, a character count.
labelstring'Markdown editor'—
disabledboolean——

Behaviour

TopicDetailDescription
a plain textareanot contenteditableRich editors reimplement undo, IME composition, spellcheck, autocorrect, mobile keyboards and every accessibility affordance a textarea gets free — and get the undo stack wrong first.
setRangeTextkeeps undoThe toolbar edits through the element, so ctrl-Z after clicking Bold does what you expect. Rewriting value from React state does not.
previewthe same rendererAn editor whose preview differs from the reader’s view teaches a formatting model your readers will not see — including where [[markdown]] stops.
wrappingselection-awareWith text selected the markers go around it; with nothing selected the caret lands between them; applying the same marker again unwraps it.
shortcutsB / I / KTab is deliberately left alone — trapping it breaks keyboard navigation out of the field.