RichTextEditor
RichTextEditor
A single, lazy-loaded WYSIWYG surface backed by TipTap — which is itself a thin ergonomic layer over ProseMirror. The base @helixui/core bundle does not pull TipTap until a <RichTextEditor> actually mounts.
Install only what you use. Starter kit covers most documents:
pnpm add @tiptap/core @tiptap/pm @tiptap/starter-kitPer-extension peers — add the ones you actually enable:
pnpm add @tiptap/extension-underline @tiptap/extension-link \ @tiptap/extension-placeholder @tiptap/extension-text-align \ @tiptap/extension-highlight @tiptap/extension-subscript \ @tiptap/extension-superscript @tiptap/extension-task-list \ @tiptap/extension-task-item @tiptap/extension-imageBasic
import { useState } from 'react';import { RichTextEditor } from '@helixui/core';
const [html, setHtml] = useState('<p>Hello, <strong>helixui</strong>.</p>');return <RichTextEditor value={html} onChange={setHtml} />;Custom toolbar
<RichTextEditor value={html} onChange={setHtml} renderToolbar={(handle, state) => ( <Stack direction="row" gap={1}> <IconButton aria-label="bold" isActive={state.active.bold} onPress={() => handle.run('bold')}>B</IconButton> <IconButton aria-label="italic" isActive={state.active.italic} onPress={() => handle.run('italic')}><i>I</i></IconButton> <Button size="sm" onPress={() => handle.setLink(prompt('URL') ?? null)}>Link</Button> </Stack> )}/>Imperative handle
const editorRef = useRef<RichTextEditorHandle>(null);
<RichTextEditor value={html} onChange={setHtml} editorRef={editorRef} />
<Button onPress={() => editorRef.current?.run('bold')}>Bold</Button><Button onPress={() => editorRef.current?.insertImage('/logo.png', 'helixui')}>Insert image</Button>Read-only / preview mode
<RichTextEditor value={html} readOnly toolbar={false} />Extending TipTap directly
import Mention from '@tiptap/extension-mention';
<RichTextEditor value={html} onChange={setHtml} tiptapExtensions={[Mention.configure({ HTMLAttributes: { class: 'mention' } })]}/>Theming
theme="auto" watches document.documentElement.dataset.theme and retunes live. Heading sizes, code blocks, blockquote rules, and the toolbar all use helixui tokens, so the editor inherits the active DNA theme automatically.
Mobile
Below the helixui mobile breakpoint (≤767px):
- Toolbar gains larger touch targets (32×32 buttons).
- Surface padding tightens.
- Default font size drops to
--helixui-font-size-sm.
Install: @helixui/core
import { RichTextEditor } from '@helixui/core'status: stable · since: 0.7.0
Tags: wysiwyg, editor, tiptap, prosemirror
Anatomy
┌── toolbar (B I U • ¶ ) ──┐├──────────────────────────┤│ editable content area ││ … │└──────────────────────────┘Layout
- display —
block - width —
fill - height —
fill - intrinsicSize —
fills available; toolbar pinned top - stackable —
false - fullBleed —
false
Visual
A WYSIWYG editor with a default toolbar (bold, italic, lists, links, code) and a content area. Lazy-loads TipTap and selected extensions. Imperative handle exposes ProseMirror APIs.
Props
| Name | Type | Default | Description |
|---|---|---|---|
value | string | '' | HTML string for the current document. |
onChange | (html: string) => void | — | Fires on every edit with the new HTML. |
onChangeText | (text: string) => void | — | Plain-text snapshot, fired alongside onChange. |
onChangeJSON | (json: unknown) => void | — | TipTap JSON snapshot, fired alongside onChange. |
onSelectionChange | (state: RichTextSelectionState) => void | — | Fires whenever the selection or active formats change. |
extensions | RichTextExtension[] | 'all' | starter | Built-in extensions to enable. Defaults to a starter preset; pass ‘all’ to enable everything. |
theme | 'light' | 'dark' | 'auto' | auto | auto follows document.documentElement.dataset.theme. |
readOnly | boolean | false | Disable editing. |
autoFocus | boolean | false | Focus the editor when it mounts. |
placeholder | string | Write something… | Empty-state hint. Requires the placeholder extension (on by default). |
toolbar | boolean | true | Show the built-in toolbar. |
renderToolbar | (handle, state) => ReactNode | — | Render a custom toolbar instead of the default. Receives the imperative handle and current selection state. |
height | number | string | 360 | Editing surface height. Numbers are treated as px. |
editorRef | Ref<RichTextEditorHandle> | — | Imperative handle for advanced control. |
tiptapExtensions | unknown[] | [] | Extra TipTap extensions appended after the built-in ones. |
onMount | (handle: RichTextEditorHandle) => void | — | Fires once mounted, with the imperative handle. |
Tokens used
color.bg.surface.default, color.bg.surface.subtle, color.bg.surface.inverse, color.border.default, color.border.focus, color.text.primary, color.text.muted, color.text.action.brand, color.text.action.danger, color.bg.action.brand.default, color.bg.action.danger.subtle, radius.md, radius.sm, font.family.sans, font.family.mono, motion.duration.fast, motion.easing.standard
Accessibility
Notes
- TipTap exposes a
role='textbox' aria-multiline='true'surface. - Toolbar buttons set
aria-pressedfor active states. - Container shows a
:focus-withinring matching helixui’s other inputs. - Respects
prefers-reduced-motionvia the global motion stylesheet.
Composes with
| Component | Relation | Note |
|---|---|---|
CodeEditor | alternative | CodeEditor for code; RichTextEditor for prose. |
Form | parent | Treat as a controlled form input. |
TextInput | sibling | |
Textarea | sibling |
Prompt examples
These are the AI prompt → JSX mappings used by the helixui prompt DSL and integrations like Cursor / Claude Code.
blog post editor
“WYSIWYG editor for a post body”
<RichTextEditor value={body} onChange={setBody} placeholder="Write…" />