React
The @domternal/react package provides composable components and hooks that wrap the headless editor with React-native APIs. Works with React 18+ and React 19.
Installation
Section titled “Installation”pnpm add @domternal/core @domternal/theme @domternal/reactImport the theme in your root CSS or entry file:
@import '@domternal/theme';Quick start
Section titled “Quick start”The recommended pattern uses the composable Domternal component with namespaced subcomponents:
import { Domternal } from '@domternal/react';import { StarterKit, BubbleMenu } from '@domternal/core';
export default function Editor() { return ( <Domternal extensions={[StarterKit, BubbleMenu]} content="<p>Hello from React!</p>" > <Domternal.Toolbar /> <Domternal.Content /> <Domternal.BubbleMenu contexts={{ text: ['bold', 'italic', 'underline'] }} /> </Domternal> );}Domternal creates the editor and provides it to all subcomponents via React Context. No need to pass editor props manually.
Two approaches
Section titled “Two approaches”Composable pattern (recommended)
Section titled “Composable pattern (recommended)”The Domternal component handles editor creation and context internally. Subcomponents access the editor automatically:
<Domternal extensions={extensions} content={content}> <Domternal.Toolbar /> <Domternal.Content /> <Domternal.BubbleMenu /> <Domternal.EmojiPicker emojis={emojis} /></Domternal>Hook pattern (full control)
Section titled “Hook pattern (full control)”Use useEditor when you need direct access to the editor instance or custom rendering:
import { useEditor, EditorProvider, DomternalToolbar } from '@domternal/react';import { StarterKit } from '@domternal/core';
export default function Editor() { const { editor, editorRef } = useEditor({ extensions: [StarterKit], content: '<p>Hello</p>', });
return ( <EditorProvider editor={editor}> {editor && <DomternalToolbar />} <div className="dm-editor"> <div ref={editorRef} /> </div> </EditorProvider> );}useEditor
Section titled “useEditor”Core hook for creating and managing an editor instance.
const { editor, editorRef } = useEditor(options, deps?);Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
extensions | AnyExtension[] | [] | Extensions to load. Default extensions (Document, Paragraph, Text, BaseKeymap, History) are always included. |
content | Content | '' | Initial content (HTML string or JSON). Reactive: changing this prop syncs the editor. |
editable | boolean | true | Whether the editor is editable. Reactive. |
autofocus | FocusPosition | false | Focus on mount: true, 'start', 'end', 'all', or a position number. |
outputFormat | 'html' | 'json' | 'html' | Format used for content comparison when syncing. |
immediatelyRender | boolean | true | Set to false for SSR (Next.js). Editor is created in useEffect instead of during render. |
onCreate | (editor: Editor) => void | - | Called when the editor is created. |
onUpdate | (props: { editor: Editor }) => void | - | Called when the document content changes. |
onSelectionChange | (props: { editor: Editor }) => void | - | Called when the selection changes without content change. |
onFocus | (props: { editor: Editor; event: FocusEvent }) => void | - | Called when the editor gains focus. |
onBlur | (props: { editor: Editor; event: FocusEvent }) => void | - | Called when the editor loses focus. |
onDestroy | () => void | - | Called before the editor is destroyed. |
Return value
Section titled “Return value”| Property | Type | Description |
|---|---|---|
editor | Editor | null | The editor instance. Null during SSR when immediatelyRender is false. |
editorRef | RefObject<HTMLDivElement> | Ref to attach to the editor mount point. |
Forced recreation with deps
Section titled “Forced recreation with deps”Pass a dependency array as the second argument. When any value changes, the editor is destroyed and recreated (content is preserved):
const { editor, editorRef } = useEditor({ extensions, content }, [locale]);// Editor recreates when locale changesuseEditorState
Section titled “useEditorState”Subscribe to editor state changes. Two overloads for different use cases.
Full state (simple)
Section titled “Full state (simple)”Returns all state properties. Re-renders when any state changes:
const { htmlContent, jsonContent, isEmpty, isFocused, isEditable } = useEditorState(editor);| Property | Type | Description |
|---|---|---|
htmlContent | string | Current document as HTML. |
jsonContent | JSONContent | null | Current document as JSON. |
isEmpty | boolean | Whether the document is empty. |
isFocused | boolean | Whether the editor has focus. |
isEditable | boolean | Whether the editor is editable. |
Selector (granular)
Section titled “Selector (granular)”Returns a derived value. Only re-renders when the selector result changes:
const isBold = useEditorState(editor, (ed) => ed.isActive('bold'));const wordCount = useEditorState(editor, (ed) => ed.state.doc.textContent.split(/\s+/).filter(Boolean).length);The selector receives the full Editor instance and returns T | undefined. The return is undefined when the editor is null or destroyed.
useCurrentEditor
Section titled “useCurrentEditor”Access the editor instance from the nearest EditorProvider or Domternal component:
import { useCurrentEditor } from '@domternal/react';
function MyCustomButton() { const { editor } = useCurrentEditor(); if (!editor) return null;
return ( <button onClick={() => editor.chain().focus().toggleBold().run()}> Bold </button> );}Components
Section titled “Components”Domternal
Section titled “Domternal”Composable root that creates an editor and provides it via context. Accepts all useEditor options as props.
All useEditor options plus:
| Prop | Type | Required | Description |
|---|---|---|---|
deps | DependencyList | No | Dependency array for forced editor recreation. |
children | ReactNode | Yes | Child components (toolbar, content, menus). |
Subcomponents
Section titled “Subcomponents”| Component | Props | Description |
|---|---|---|
Domternal.Content | className?: string | Renders the editor content area. Mounts the ProseMirror DOM. |
Domternal.Loading | children: ReactNode | Renders children only while editor is not ready (SSR loading state). |
Domternal.Toolbar | icons?: IconSet, layout?: ToolbarLayoutEntry[] | Auto-rendering toolbar with optional custom icons and layout. |
Domternal.BubbleMenu | items?, contexts?, shouldShow?, placement?, offset?, updateDelay?, children? | Context-aware bubble menu. See DomternalBubbleMenu. |
Domternal.FloatingMenu | shouldShow?, offset?, children? | Floating menu on empty lines. See DomternalFloatingMenu. |
Domternal.EmojiPicker | emojis: EmojiPickerItem[] | Searchable emoji picker. See DomternalEmojiPicker. |
All subcomponents automatically use the editor from context. No editor prop needed.
DomternalEditor
Section titled “DomternalEditor”All-in-one editor with integrated state management, context provider, and controlled mode support. Alternative to the composable pattern.
import { useRef } from 'react';import { DomternalEditor, DomternalToolbar } from '@domternal/react';import type { DomternalEditorRef } from '@domternal/react';
export default function Editor() { const ref = useRef<DomternalEditorRef>(null);
return ( <DomternalEditor ref={ref} extensions={[StarterKit]} content="<p>Hello</p>" onUpdate={({ editor }) => console.log(editor.getHTML())} > <DomternalToolbar /> </DomternalEditor> );}All useEditor options plus:
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | CSS class for the .dm-editor wrapper. |
outputFormat | 'html' | 'json' | 'html' | Format used by onChange and content comparison. |
value | Content | - | Controlled mode: editor syncs to this value. |
onChange | (value: string | JSONContent) => void | - | Controlled mode: called on content changes. Returns HTML string or JSON based on outputFormat. |
children | ReactNode | - | Rendered inside the provider (toolbar, menus). |
Ref (DomternalEditorRef)
Section titled “Ref (DomternalEditorRef)”| Property | Type | Description |
|---|---|---|
editor | Editor | null | The editor instance. |
htmlContent | string | Current HTML content. |
jsonContent | JSONContent | null | Current JSON content. |
isEmpty | boolean | Whether the document is empty. |
isFocused | boolean | Whether the editor has focus. |
isEditable | boolean | Whether the editor is editable. |
Controlled mode
Section titled “Controlled mode”const [html, setHtml] = useState('<p>Hello</p>');
<DomternalEditor extensions={[StarterKit]} value={html} onChange={setHtml} outputFormat="html"/>DomternalToolbar
Section titled “DomternalToolbar”Auto-renders toolbar buttons, dropdowns, separators, and keyboard navigation based on the editor’s extensions.
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | from context | The editor instance. Falls back to context if omitted. |
icons | IconSet | undefined | Custom icon set. Falls back to built-in defaultIcons. |
layout | ToolbarLayoutEntry[] | undefined | Custom layout to reorder, group, or filter toolbar items. |
Layout
Section titled “Layout”By default, the toolbar renders all items grouped by their group property. Use layout to customize:
import type { ToolbarLayoutEntry } from '@domternal/core';
const layout: ToolbarLayoutEntry[] = [ 'bold', 'italic', 'underline', '|', 'heading', '|', 'bulletList', 'orderedList',];
<Domternal.Toolbar layout={layout} />Keyboard navigation
Section titled “Keyboard navigation”| Key | Action |
|---|---|
ArrowRight | Focus next button |
ArrowLeft | Focus previous button |
Home | Focus first button |
End | Focus last button |
Escape | Close open dropdown |
DomternalBubbleMenu
Section titled “DomternalBubbleMenu”An inline formatting toolbar that appears when the user selects text.
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | from context | The editor instance. |
items | string[] | - | Fixed list of item names to show. Use `' |
contexts | Record<string, string[] | true | null> | - | Context-aware items. Key is a node type name or 'text'/'table'. Use `' |
shouldShow | (props: { editor: Editor; view: EditorView; state: EditorState; from: number; to: number }) => boolean | - | Custom predicate to control visibility. |
placement | 'top' | 'bottom' | 'top' | Menu placement relative to the selection. |
offset | number | 8 | Pixel offset from the selection. |
updateDelay | number | 0 | Delay in milliseconds before updating position. |
children | ReactNode | - | Additional content rendered after buttons. |
Context-aware menus
Section titled “Context-aware menus”Show different buttons depending on what the user selected:
<Domternal.BubbleMenu contexts={{ text: ['bold', 'italic', 'underline', '|', 'link'], heading: ['bold', 'italic', '|', 'link'], codeBlock: null, image: ['imageFloatLeft', 'imageFloatCenter', 'imageFloatRight', '|', 'deleteImage'], }}/>text- Default for text selectionsnull- Hide the menu in that contexttrue- Show all valid items for that context
Custom content
Section titled “Custom content”Add custom buttons alongside the auto-rendered ones:
<Domternal.BubbleMenu contexts={{ text: ['bold', 'italic'] }}> <button onClick={handleCustomAction}>Custom</button></Domternal.BubbleMenu>DomternalFloatingMenu
Section titled “DomternalFloatingMenu”A menu that appears on empty lines, allowing users to insert block-level content.
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | from context | The editor instance. |
shouldShow | (props: { editor: Editor; view: EditorView; state: EditorState }) => boolean | - | Custom predicate to control visibility. Default: shows on empty paragraphs. |
offset | number | 0 | Pixel offset from the cursor position. |
children | ReactNode | - | Content to render inside the menu. |
<Domternal.FloatingMenu> <button onClick={() => editor.commands.setHeading({ level: 1 })}>H1</button> <button onClick={() => editor.commands.toggleBulletList()}>List</button> <button onClick={() => editor.commands.setHorizontalRule()}>Divider</button></Domternal.FloatingMenu>DomternalEmojiPicker
Section titled “DomternalEmojiPicker”A searchable emoji picker panel that opens from the toolbar emoji button.
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | from context | The editor instance. |
emojis | EmojiPickerItem[] | required | Array of emoji definitions with emoji, name, and group properties. Import the type from @domternal/react. |
import { Emoji, emojis } from '@domternal/extension-emoji';import type { EmojiPickerItem } from '@domternal/react';
<Domternal extensions={[StarterKit, Emoji]}> <Domternal.Toolbar /> <Domternal.Content /> <Domternal.EmojiPicker emojis={emojis} /></Domternal>The picker opens when the user clicks the emoji toolbar button. It includes search, category tabs, frequently used section, and smooth scrolling.
EditorContent
Section titled “EditorContent”Low-level component that mounts the ProseMirror DOM into a div. Used with the useEditor hook pattern when you need full control over the layout.
import { useEditor, EditorContent } from '@domternal/react';import { StarterKit } from '@domternal/core';
export default function Editor() { const { editor } = useEditor({ extensions: [StarterKit], content: '<p>Hello</p>' }); return <EditorContent editor={editor} className="my-editor" />;}| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | required | The editor instance to render. |
innerRef | Ref<HTMLDivElement> | - | Ref to the underlying DOM container. |
Plus all standard HTMLDivElement attributes (className, style, id, etc.).
Custom node views
Section titled “Custom node views”Render React components inside the editor document. This lets you build interactive, editable nodes with full React lifecycle support.
ReactNodeViewRenderer
Section titled “ReactNodeViewRenderer”Converts a React component into a ProseMirror NodeView constructor:
import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent, useReactNodeView } from '@domternal/react';import { Node } from '@domternal/core';import type { ReactNodeViewProps } from '@domternal/react';
function CalloutComponent({ node, updateAttributes }: ReactNodeViewProps) { return ( <NodeViewWrapper className={`callout callout-${node.attrs['type']}`}> <select value={node.attrs['type'] as string} onChange={(e) => updateAttributes({ type: e.target.value })} contentEditable={false} > <option value="info">Info</option> <option value="warning">Warning</option> <option value="error">Error</option> </select> <NodeViewContent /> </NodeViewWrapper> );}
const Callout = Node.create({ name: 'callout', group: 'block', content: 'inline*',
addAttributes() { return { type: { default: 'info' } }; },
parseHTML() { return [{ tag: 'div[data-callout]' }]; },
renderHTML({ node, HTMLAttributes }) { return ['div', { ...HTMLAttributes, 'data-callout': node.attrs['type'] }, 0]; },
addNodeView() { return ReactNodeViewRenderer(CalloutComponent); },});ReactNodeViewProps
Section titled “ReactNodeViewProps”| Prop | Type | Description |
|---|---|---|
editor | Editor | The editor instance. |
node | PMNode | The ProseMirror node being rendered. |
selected | boolean | Whether this node is selected via NodeSelection. |
getPos | () => number | Get the document position of this node. |
updateAttributes | (attrs) => void | Update the node’s attributes. |
deleteNode | () => void | Delete this node from the document. |
extension | { name, options } | The extension that created this node view. |
decorations | unknown[] | ProseMirror decorations applied to this node. |
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
as | string | 'div' / 'span' | Wrapper element tag. Defaults to 'div' for block nodes, 'span' for inline. |
className | string | - | CSS class on the wrapper element. |
contentDOMElement | string | null | 'div' | Tag for the content DOM element. Set to null for nodes with no editable content. |
NodeViewWrapper
Section titled “NodeViewWrapper”Container component for custom node views. Handles drag events and marks the element as a node view wrapper.
<NodeViewWrapper as="section" className="my-node"> {/* Node view UI */}</NodeViewWrapper>| Prop | Type | Default | Description |
|---|---|---|---|
as | ElementType | 'div' | HTML element type to render. |
Plus all standard HTML element attributes.
NodeViewContent
Section titled “NodeViewContent”Placeholder for editable nested content. ProseMirror manages the DOM inside this element.
<NodeViewContent as="p" className="my-content" />| Prop | Type | Default | Description |
|---|---|---|---|
as | ElementType | 'div' | HTML element type to render. |
Plus all standard HTML element attributes.
useReactNodeView
Section titled “useReactNodeView”Access node view context inside NodeViewWrapper and NodeViewContent:
import { useReactNodeView } from '@domternal/react';
function MyNodeView() { const { onDragStart, nodeViewContentRef } = useReactNodeView(); // onDragStart: drag event handler // nodeViewContentRef: ref callback for content DOM}SSR (Next.js)
Section titled “SSR (Next.js)”Set immediatelyRender to false to prevent the editor from being created during server-side rendering:
<Domternal extensions={[StarterKit]} content="<p>Hello</p>" immediatelyRender={false}> <Domternal.Loading> <div>Loading editor...</div> </Domternal.Loading> <Domternal.Toolbar /> <Domternal.Content /></Domternal>Domternal.Loading renders its children only while the editor is null (during SSR and before useEffect runs). Once the editor is created, the loading content disappears and the toolbar and content are rendered.
For SSR content generation without a browser, use the core SSR helpers:
import { generateHTML, generateJSON, generateText } from '@domternal/react';// These are re-exported from @domternal/core for convenienceDark mode
Section titled “Dark mode”Apply the theme class to a parent element:
{/* Always dark */}<div className="dm-theme-dark"> <Domternal extensions={extensions}> <Domternal.Toolbar /> <Domternal.Content /> </Domternal></div>
{/* Follow system preference */}<div className="dm-theme-auto"> <Domternal extensions={extensions}> <Domternal.Toolbar /> <Domternal.Content /> </Domternal></div>Toggle at runtime:
function toggleTheme() { document.body.classList.toggle('dm-theme-dark');}Custom icons
Section titled “Custom icons”Replace the built-in Phosphor icons with your own:
import type { IconSet } from '@domternal/core';
const customIcons: IconSet = { textB: '<svg>...</svg>', textItalic: '<svg>...</svg>',};
<Domternal.Toolbar icons={customIcons} />When icons is provided, the component uses only that set. To override just a few icons, spread defaultIcons:
import { defaultIcons } from '@domternal/core';
const myIcons = { ...defaultIcons, textB: '<svg><!-- custom bold --></svg>' };Component reference
Section titled “Component reference”All components can be imported individually or used as Domternal subcomponents:
// Individual importsimport { useEditor, useEditorState, useCurrentEditor, EditorProvider, Domternal, DomternalEditor, EditorContent, DomternalToolbar, DomternalBubbleMenu, DomternalFloatingMenu, DomternalEmojiPicker, ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent, useReactNodeView, DEFAULT_EXTENSIONS,} from '@domternal/react';Re-exported types from @domternal/core for convenience:
import type { Content, AnyExtension, FocusPosition, JSONContent } from '@domternal/react';import { Editor } from '@domternal/react';import { generateHTML, generateJSON, generateText } from '@domternal/react';Requirements
Section titled “Requirements”- React 18.0.0 or later (React 19 supported)
@domternal/core0.4.0 or later