Inputs
RichTextEditor
A contentEditable rich text editor with a formatting toolbar, tables, images, text color/highlight/font-size, Notion-style markdown shortcuts, and a slash command menu — all sanitized through the same DOMPurify allowlist on every emitted change.
RichTextEditorPreview
<h1>Getting started</h1><p>This editor supports <b>bold</b>, <i>italic</i>, tables, images, and more. Try typing <code>/</code> at the start of a line, or <code># </code> for a heading.</p>
Usage
tsx
import { useState } from "react";
import { RichTextEditor } from "@vesture/react";
function Example() {
const [html, setHtml] = useState("<p>Write something…</p>");
return (
<RichTextEditor
value={html}
onChange={setHtml}
onImageUpload={async (file) => {
const url = await uploadToMyBackend(file);
return url;
}}
/>
);
}
// Trim the toolbar for a simpler use case:
// <RichTextEditor toolbar={["bold", "italic", "link", "bulletList"]} />Behavior
- Text formatting: Bold, Italic, Underline, Strikethrough, Heading 1/2, Bullet/Numbered list, Blockquote, Link (opens a small inline popover for the URL instead of window.prompt), Undo/Redo (the browser's native contentEditable undo stack — verified reliable for this editor's edit patterns, not reimplemented).
- Alignment: Left/Center/Right/Justify — each toolbar button reflects the current block's actual alignment as its pressed state, same active-state pattern as Bold/Italic.
- Text color & highlight: two toolbar buttons open a curated swatch grid (the library's own chart series1–8 palette plus Default text/Danger/Success/Warning) rather than a full color picker — click a swatch to apply. Produces a <span style="color:..."> / style="background-color:..."> — the sanitizer allows only the color, background-color, and font-size CSS properties in any style attribute, nothing else, so this can't be used to smuggle arbitrary CSS.
- Font size: a dropdown of Small/Normal/Medium/Large/Extra large, mapped to the token contract's font.sizeXs–sizeXl — not a raw pixel input, so sizes stay on the same scale as the rest of the library. Internally resolves the token to its actual computed pixel value before writing it out, so the exported HTML is portable (renders correctly even outside a Vesture-themed page).
- Tables: the ⊞ toolbar button opens a Word/Google-Docs-style hover grid — hover to preview an N×M size, click to insert. Click into any cell to reveal a small ⋮ handle at its corner with Insert row above/below, Insert column left/right, Delete row, Delete column, and Delete table.
- Images: the 🖼 toolbar button opens a popover with two ways to add an image — paste a URL directly, or drag/drop or browse a file (reuses the FileUpload component's dropzone). Without an onImageUpload prop, uploaded files are embedded directly as base64 data URLs (fine for small images; bloats the HTML for large ones — see the prop table). Click an inserted image to select it, then drag the handle at its corner to resize; hold Shift while dragging to resize freely instead of preserving aspect ratio.
- Markdown shortcuts: typed at the very start of an empty block, these convert it and remove the trigger text automatically — "# " → Heading 1, "## " → Heading 2, "* " or "- " → bullet list, "1. " → numbered list, "> " → blockquote, "```" → a preformatted code block. They never fire mid-sentence, only when the block has nothing else in it.
- Slash command menu: typing "/" at the start of an empty block opens an inline menu anchored right at the text cursor (not centered on screen). Keep typing to filter ("/tab" narrows to "Table"), Arrow keys move the selection, Enter applies it, Escape or typing something that matches nothing closes the menu without applying anything. Lists the same commands as the toolbar: headings, lists, blockquote, table, image.
- Paste handling: pasted HTML (from Word, Google Docs, a webpage) is stripped of its inline-style/class cruft and re-sanitized through the exact same allowlist used for onChange — never a second, looser path. Ctrl/Cmd+Shift+V pastes as plain text instead, stripping all formatting, for when you explicitly don't want the source's styling.
- Every emitted onChange value is sanitized HTML, never the raw contentEditable innerHTML — the toolbar/table/image/color/font-size allowlist is deliberately narrow (exactly what these features can produce, nothing broader), and was specifically verified against real injection attempts (script tags, event-handler attributes, javascript: URLs, and CSS-based attacks like expression()) during development, not just happy-path formatting.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value / defaultValue / onChange | string | — | Controlled/uncontrolled HTML. value is always sanitized before being written into the editor; onChange always emits sanitized HTML. |
| placeholder | string | — | Shown via CSS :empty:before when the editor has no text and no table/image content. |
| disabled | boolean | — | Disables the editing surface and every toolbar button. |
| toolbar | ToolbarCommand[] | DEFAULT_TOOLBAR (all commands) | Trims the toolbar to a subset, e.g. ["bold", "italic", "link"]. |
| onImageUpload | (file: File) => Promise<string> | — | If provided, its resolved URL is used as the <img> src instead of a base64 data URL — wire this to your own backend/CDN upload. |
| aria-label | string | "Rich text editor" | Accessible name for the contentEditable surface (role="textbox"). |