@rtif-sdk/react
v2.14.0
Published
React bindings for RTIF — zero-config to full-control
Maintainers
Readme
@rtif-sdk/react
React bindings for RTIF with zero-config to full-control DX.
import { Editor } from '@rtif-sdk/react';
import '@rtif-sdk/react/styles/all.css';
function App() {
return (
<Editor
preset="standard"
placeholder="Start writing..."
onChange={({ html, isEmpty }) => save(html)}
formats={['html']}
/>
);
}Install
npm install @rtif-sdk/react @rtif-sdk/core @rtif-sdk/engine @rtif-sdk/web
# Optional format plugins (for onChange serialization, defaultHTML, etc.)
npm install @rtif-sdk/format-html @rtif-sdk/format-markdownRequires React 18 or 19 as a peer dependency.
Styles
Import the aggregated stylesheet once at your app's entry point:
import '@rtif-sdk/react/styles/all.css';This re-exports @rtif-sdk/web/styles/all.css which includes all component styles (tokens, marks, blocks, toolbar, pickers, features). All rules use :where() zero-specificity, so your CSS always wins.
For dark mode, add the optional dark theme after:
import '@rtif-sdk/react/styles/all.css';
import '@rtif-sdk/react/styles/dark.css';Dark mode activates automatically via prefers-color-scheme: dark, the rtif-dark class, or the dark class (Tailwind/shadcn convention).
All CSS source lives in
@rtif-sdk/web/styles/. The React package re-exports for convenience. For selective imports and the full CSS reference, seedocs/styling.mdin the@rtif-sdk/webpackage.
Usage Modes
Zero config
Renders a default toolbar and content area. No plugins needed for basic text editing.
<Editor />Preset
Named preset bundles (plaintext, basic, standard, full) provide curated plugin sets:
<Editor preset="standard" placeholder="Type here..." />| Preset | Plugins |
|--------|---------|
| plaintext | None |
| basic | bold, italic, underline, link |
| standard | basic + strikethrough, code, heading, list, blockquote, codeBlock, hr |
| full | standard + image, callout, embed, alignment, indent, textColor, backgroundColor, fontSize, fontFamily |
Explicit plugins
For full control, pass an array of plugin factories:
import { Editor, bold, italic, heading, link, preset } from '@rtif-sdk/react';
// Individual plugins
<Editor plugins={[bold(), italic(), heading(), link()]} />
// Preset with surgical overrides
<Editor plugins={preset('full', {
overrides: { 'block-image': { upload: myUploader } },
remove: ['block-embed'],
})} />Compound mode
When <Editor.Content> is a child, auto-layout is disabled and you control the layout:
<Editor plugins={preset('standard')}>
<Editor.Toolbar />
<Editor.BubbleToolbar />
<Editor.Content className="min-h-[300px] p-4" />
</Editor>Headless mode
Use useEditor + EditorProvider for maximum flexibility:
import { useEditor, EditorProvider, EditorContent, preset } from '@rtif-sdk/react';
function MyEditor() {
const editor = useEditor({ plugins: preset('standard') });
return (
<EditorProvider editor={editor}>
<MyCustomToolbar />
<EditorContent />
</EditorProvider>
);
}Compound Sub-Components
| Component | Description |
|-----------|-------------|
| Editor.Content | The contenteditable area |
| Editor.Toolbar | Default formatting toolbar — supports append children and render-prop customization |
| Editor.BubbleToolbar | Floating toolbar that appears on text selection |
| Editor.MentionMenu | Render-prop for mention dropdown UI |
| Editor.SlashMenu | Render-prop for slash command menu UI |
Smart onChange
The onChange callback is debounced and serializes only the formats you request:
<Editor
preset="standard"
onChange={({ doc, html, text, markdown, isEmpty, state }) => {
console.log(html); // only present if 'html' is in formats
console.log(isEmpty); // true when document is empty
}}
formats={['html', 'text']}
debounceMs={500} // default: 300
transientAttrs={['focused']} // strip before serialization
/>Requires the corresponding format plugin to be installed (@rtif-sdk/format-html, @rtif-sdk/format-markdown). Serializers are loaded lazily on first use.
Initial Content
// From an RTIF document
<Editor defaultDoc={savedDoc} />
// From HTML (requires @rtif-sdk/format-html)
<Editor defaultHTML="<p>Hello <strong>world</strong></p>" />Imperative Ref
import { useRef } from 'react';
import type { EditorRef } from '@rtif-sdk/react';
function MyEditor() {
const ref = useRef<EditorRef>(null);
return (
<>
<button onClick={() => ref.current?.focus()}>Focus</button>
<button onClick={() => console.log(ref.current?.getHTML())}>Log HTML</button>
<button onClick={() => ref.current?.clear()}>Clear</button>
<Editor ref={ref} preset="standard" />
</>
);
}EditorRef methods
| Method | Returns | Description |
|--------|---------|-------------|
| focus() | void | Focus the editor |
| blur() | void | Blur the editor |
| isFocused() | boolean | Whether the editor is focused |
| getDoc() | Document | Current RTIF document |
| getSelection() | Selection | Current selection |
| getHTML() | string | HTML serialization (requires format-html) |
| getText() | string | Plain text serialization (requires format-plaintext) |
| isEmpty() | boolean | Whether the document is empty |
| exec(cmd, payload?) | void | Execute a named command |
| isActive(cmd, payload?) | boolean | Whether a command's effect is active |
| canExecute(cmd, payload?) | boolean | Whether a command can execute |
| clear() | void | Clear all content |
Subscription Hooks
Reactive hooks for building custom UI. All use useSyncExternalStore for tear-free reads.
import {
useIsActive,
useCanExecute,
useMarksAtCursor,
useBlockAtCursor,
useEditorState,
useEditorFocused,
useFocusedBlock,
} from '@rtif-sdk/react';
function MyToolbar() {
const editor = useEditorContext();
const isBold = useIsActive(editor, 'toggleMark:bold');
const canUndo = useCanExecute(editor, 'undo');
const marks = useMarksAtCursor(editor);
const block = useBlockAtCursor(editor);
const focused = useEditorFocused(editor);
return (
<button
aria-pressed={isBold}
onMouseDown={(e) => { e.preventDefault(); editor.exec('toggleMark:bold'); }}
>
Bold
</button>
);
}Toolbar Components
Pre-built toolbar primitives that auto-reflect active/disabled state:
import {
Toolbar,
ToolbarButton,
ToolbarSeparator,
ToolbarGroup,
BoldButton,
ItalicButton,
HeadingSelect,
DefaultToolbar,
BubbleToolbar,
ColorPicker,
FontSizePicker,
FontFamilyPicker,
} from '@rtif-sdk/react';
// Use pre-built buttons
<Toolbar>
<BoldButton />
<ItalicButton />
<ToolbarSeparator />
<ToolbarButton command="toggleMark:underline" label="Underline">U</ToolbarButton>
</Toolbar>
// Or the all-in-one default toolbar
<DefaultToolbar />
// Floating selection toolbar
<BubbleToolbar />
// Block type dropdown
<HeadingSelect />
// Pickers
<ColorPicker />
<FontSizePicker />
<FontFamilyPicker />Customizing the Default Toolbar
<DefaultToolbar> (also available as <Editor.Toolbar>) supports three modes:
// 1. Default — standard layout, no customization
<Editor.Toolbar />
// 2. Append — add custom elements after the defaults
<Editor.Toolbar>
<ColorPicker />
<FontSizePicker />
</Editor.Toolbar>
// 3. Render prop — full layout control via sections object
<Editor.Toolbar>
{(sections) => (
<>
{sections.undoRedo}
<ToolbarSeparator />
<MyCustomHeadingSelect />
<ToolbarSeparator />
{sections.formatMarks}
</>
)}
</Editor.Toolbar>The render prop exposes pre-built groups (undoRedo, headingSelect, formatMarks, link) and individual buttons (undo, redo, bold, italic, underline, strikethrough, code).
Two helpers simplify common patterns:
// sections.group() — render sections with auto-separators
{sections.group('undoRedo', 'formatMarks', 'link')}
// sections.without() — default layout minus excluded sections
{sections.without('headingSelect')}Mention Support
<Editor
preset="standard"
mention={{
triggerChar: '@',
onKeyDown: (session, event) => {
if (event.key === 'Enter') {
session.accept({ id: '1', displayName: 'Alice' });
return true;
}
return false;
},
}}
>
<Editor.Content />
<Editor.MentionMenu>
{({ isActive, query, triggerRect, accept, dismiss }) =>
isActive && (
<MentionDropdown
query={query}
position={triggerRect}
onSelect={(user) => accept({ id: user.id, displayName: user.name })}
onClose={dismiss}
/>
)
}
</Editor.MentionMenu>
</Editor>Slash Commands
import { filterSlashItems } from '@rtif-sdk/react';
<Editor
preset="standard"
slashMenu={{
triggerChar: '/',
items: DEFAULT_SLASH_ITEMS, // or custom items
}}
>
<Editor.Content />
<Editor.SlashMenu>
{({ isActive, query, items, triggerRect, accept, dismiss }) =>
isActive && (
<SlashCommandMenu
items={filterSlashItems(items, query)}
position={triggerRect}
onSelect={(item) => accept(item)}
onClose={dismiss}
/>
)
}
</Editor.SlashMenu>
</Editor>Read-Only Document Rendering
For feeds, previews, and static content display without editor overhead:
import { DocumentView, DocumentViewProvider } from '@rtif-sdk/react';
// Single document
<DocumentView doc={post.doc} />
// Shared config across multiple views
<DocumentViewProvider marks={{ mention: mentionRenderer }}>
{posts.map(p => <DocumentView key={p.id} doc={p.doc} />)}
</DocumentViewProvider>Multi-Block Selection
All mark and block type commands automatically handle multi-block selections. When the user selects across multiple blocks:
- Format buttons (bold, italic, color, etc.) apply to all selected blocks
- Block type buttons (heading, blockquote, list, etc.) change all selected blocks
- Undo reverts the entire multi-block change in one step
Configure how the toolbar reports active state for mixed-type selections via blockTypeActiveStrategy on the engine:
const editor = useEditor({
plugins: preset('standard'),
blockTypeActiveStrategy: 'first', // or 'all' (default)
});| Strategy | Behavior | Use case |
|----------|----------|----------|
| 'all' | Active only when every selected block matches | Conservative default |
| 'first' | Active when the first selected block matches | Google Docs/Notion behavior |
Plugin System
Built-in plugin factories
Every plugin is a factory function returning a unified EditorPlugin:
import { bold, image, link } from '@rtif-sdk/react';
bold() // EditorPlugin<BoldConfig>
image({ upload: fn }) // EditorPlugin<ImageConfig>
link() // EditorPlugin<LinkConfig>Surgical overrides via extend()
const customImage = image({ upload: myUploader }).extend({ maxFileSize: 10_000_000 });Preset customization
const plugins = preset('full', {
overrides: {
'block-image': { upload: myUploader },
},
remove: ['block-embed'],
add: [myCustomPlugin()],
});Creating custom plugins
Use createEditorPlugin to reduce boilerplate:
import { createEditorPlugin } from '@rtif-sdk/react';
interface MyConfig {
color?: string;
}
export const myPlugin = createEditorPlugin<MyConfig>('my-plugin', (config) => ({
engine: createMyEnginePlugin(config),
marks: { myMark: myRenderer },
}));
// Usage
const plugins = [myPlugin({ color: 'red' })];EditorFeatures (UI plugins)
UI features like block gutter, context menu, and link popover are tree-shakeable opt-ins:
import { blockGutter, contextMenu, linkPopover, defaultFeatures } from '@rtif-sdk/react';
<Editor
plugins={preset('standard')}
features={[blockGutter(), contextMenu(), linkPopover()]}
/>
// Or use the convenience helper
<Editor plugins={preset('standard')} features={defaultFeatures()} />blockDrag() provides drag-to-reorder with keyboard support and a theme-aware preview.
Use it as an alternative to blockGutter — do not use both at the same time.
import { Editor, blockDrag } from '@rtif-sdk/react';
// Basic
<Editor features={[blockDrag()]} />
// With custom preview config
<Editor features={[blockDrag({ preview: { maxWidth: 400 }, keyboard: true })]} />Alt+ArrowUp / Alt+ArrowDown reorder the focused block without dragging.
CSS Custom Properties
All visual tokens are overridable via CSS custom properties. The toolbar-specific tokens defined by this package:
:where(.rtif-toolbar) {
--rtif-toolbar-bg: var(--rtif-surface, #fff);
--rtif-toolbar-border: var(--rtif-border, #ccc);
--rtif-toolbar-gap: 2px;
--rtif-toolbar-padding: 4px 8px;
--rtif-toolbar-button-size: 32px;
--rtif-toolbar-button-radius: 4px;
--rtif-toolbar-button-hover: var(--rtif-hover-bg, #e8e8e8);
--rtif-toolbar-button-active: var(--rtif-active-bg, #d0e0ff);
--rtif-content-padding: 16px;
--rtif-content-font-size: 16px;
--rtif-content-line-height: 1.6;
}Base design tokens (--rtif-accent-color, --rtif-surface, --rtif-border, --rtif-shadow, --rtif-focus-ring), interactive state tokens (--rtif-text-color, --rtif-hover-bg, --rtif-active-bg, --rtif-danger-color, etc.), and content tokens (--rtif-code-bg, --rtif-code-block-bg, --rtif-blockquote-text, --rtif-placeholder-color, etc.) are defined by @rtif-sdk/web and inherited — not redeclared.
Programmatic Theming
The theme prop sets CSS custom properties as inline styles without writing CSS:
<Editor
theme={{ surface: '#1a1a1a', textColor: '#e0e0e0', accentColor: '#4a9eff' }}
preset="standard"
/>The EditorTheme interface supports all 15 tokens. See the Styling Guide for the full key-to-property mapping.
Testing
A separate entry point provides test utilities:
import { createMockEditorHandle, createDefaultDoc, isDocEmpty } from '@rtif-sdk/react/testing';
const editor = createMockEditorHandle();
// Use with <EditorContext.Provider value={editor}> in testsAPI Reference
Components
| Export | Description |
|--------|-------------|
| Editor | Compound component with auto-layout |
| EditorContent | Contenteditable container |
| EditorProvider | Headless context provider |
| DocumentView | Read-only document renderer |
| DocumentViewProvider | Shared config for multiple DocumentViews |
| Toolbar | Toolbar container (role="toolbar") |
| ToolbarButton | Command button with active/disabled state |
| ToolbarSeparator | Visual divider |
| ToolbarGroup | Button grouping |
| DefaultToolbar | Pre-composed toolbar with common buttons (supports append and render-prop children) |
| HeadingSelect | Block type dropdown |
| BubbleToolbar | Floating selection toolbar |
| ColorPicker | Text color picker |
| FontSizePicker | Font size picker |
| FontFamilyPicker | Font family picker |
| BoldButton ... RedoButton | Pre-built format buttons |
Hooks
| Export | Description |
|--------|-------------|
| useEditor | Core lifecycle hook — creates engine + handle |
| useEditorContext | Access EditorHandle from context |
| useIsActive | Whether a command effect is active |
| useCanExecute | Whether a command can execute |
| useMarksAtCursor | Marks at the current cursor offset |
| useBlockAtCursor | Block at the current cursor offset |
| useEditorState | Full editor state (re-renders on every change) |
| useEditorFocused | Whether the editor is focused |
| useFocusedBlock | Currently focused block info |
| useSmartOnChange | Debounced, serialized onChange subscription |
| useMentionPlugin | Headless mention trigger hook |
| useSlashCommandPlugin | Headless slash command hook |
| useTrigger | Generic trigger hook |
| useSavedSelection | Saved selection range and text, with save/clear actions |
Plugin Factories
| Export | ID | Type |
|--------|----|------|
| bold() | mark-bold | Mark |
| italic() | mark-italic | Mark |
| underline() | mark-underline | Mark |
| strikethrough() | mark-strikethrough | Mark |
| code() | mark-code | Mark |
| textColor() | mark-textColor | Mark |
| fontSize() | mark-fontSize | Mark |
| fontFamily() | mark-fontFamily | Mark |
| link() | mark-link | Mark |
| heading() | block-heading | Block |
| list() | block-list | Block |
| blockquote() | block-blockquote | Block |
| codeBlock() | block-codeBlock | Block |
| hr() | block-hr | Block |
| image() | block-image | Block |
| callout() | block-callout | Block |
| embed() | block-embed | Block |
| alignment() | attr-alignment | Attr |
| indent() | attr-indent | Attr |
Utilities
| Export | Description |
|--------|-------------|
| preset(name, options?) | Resolve a preset name to plugins |
| createEditorPlugin(id, build) | Factory helper for custom plugins |
| createDefaultDoc() | Create an empty RTIF document |
| isDocEmpty(doc) | Check if a document is empty |
| filterSlashItems(items, query) | Filter slash command items by query |
| getAvailableItems(items, ctx) | Filter items by availability |
| themeToStyle(theme) | Convert EditorTheme to CSS custom property style object |
| blockGutter(config?) | EditorFeature: hover-revealed gutter with drag handle and add-block button |
| contextMenu(config?) | EditorFeature: right-click context menu |
| linkPopover(config?) | EditorFeature: inline link editing popover |
| blockDrag(config?) | EditorFeature: drag-to-reorder blocks with keyboard support and theme-aware preview |
| defaultFeatures(config?) | Convenience helper returning [blockGutter(), contextMenu(), linkPopover()] |
License
MIT
