RichTextEditor
Variants
Switch between default, subtle, and compact variants using the variant prop.
| Variant | Look | Best for |
|---|---|---|
default | Bordered container with classic joined segmented toolbar buttons | Standard editor experiences where the toolbar should feel distinct from content |
subtle | No visible border — uses a soft shadow. Muted, rounded toolbar buttons. Elevated card feel. | Forms, settings pages, or anywhere a less prominent editor fits |
compact | Muted toolbar fused with content area. Minimal spacing, smaller icons. | Sidebars, modals, inline editing — constrained spaces |
default
subtle
compact
Copy <RichTextEditor editor={editor} variant="default"> <RichTextEditor.Toolbar> {/* toolbar controls */} </RichTextEditor.Toolbar> <RichTextEditor.Content /> </RichTextEditor> <RichTextEditor editor={editor} variant="subtle"> <RichTextEditor.Toolbar> {/* toolbar controls */} </RichTextEditor.Toolbar> <RichTextEditor.Content /> </RichTextEditor> <RichTextEditor editor={editor} variant="compact"> <RichTextEditor.Toolbar> {/* toolbar controls */} </RichTextEditor.Toolbar> <RichTextEditor.Content /> </RichTextEditor>
Root className
Pass Tailwind classes to the root component via className. They are merged using tailwind-merge.
Copy <RichTextEditor editor={editor} className="rounded-xl shadow-lg ring-1 ring-black/5" > <RichTextEditor.Toolbar> {/* toolbar controls */} </RichTextEditor.Toolbar> <RichTextEditor.Content /> </RichTextEditor>
Scoped CSS Variables
Wrap the editor in a div with custom CSS variables to scope a theme to a single instance.
Copy <div style={{ "--primary": "oklch(0.6 0.2 25)", "--radius": "0.75rem", "--border": "oklch(0.6 0.15 25 / 0.3)", "--accent": "oklch(0.9 0.05 25)", }}> <RichTextEditor editor={editor}> <RichTextEditor.Toolbar> {/* toolbar controls */} </RichTextEditor.Toolbar> <RichTextEditor.Content /> </RichTextEditor> </div>
Sticky Toolbar
Pin the toolbar to the top of the viewport on scroll. Customize the background with className.
Copy <RichTextEditor editor={editor}> <RichTextEditor.Toolbar sticky stickyOffset={0} className="bg-accent/30 backdrop-blur-sm" > {/* toolbar controls */} </RichTextEditor.Toolbar> <RichTextEditor.Content /> </RichTextEditor>
Custom Toolbar Controls
Add your own buttons to the toolbar using useRichTextEditorContext and RichTextEditor.Control.
Copy import { useRichTextEditorContext } from "@editorcn/editor"; function InsertStar() { const { editor } = useRichTextEditorContext(); return ( <RichTextEditor.Control onClick={() => editor?.chain().focus().insertContent("⭐ ").run()} title="Insert star emoji" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className="rte-editor-icon"> <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" /> </svg> </RichTextEditor.Control> ); } // In your toolbar: <RichTextEditor.ControlsGroup> <InsertStar /> </RichTextEditor.ControlsGroup>
Read-only Mode
Both editors support read-only mode. When disabled, the toolbar and drag handles are hidden, and content is displayed as plain document.
RichTextEditor
Use the editable prop on RichTextEditor — the toolbar is hidden automatically when false, and the editor state is synced:
Copy const editor = useEditor({ editable: false, extensions: [StarterKit, Link], }); <RichTextEditor editor={editor} editable={false}> <RichTextEditor.Toolbar> {/* hidden when editable is false */} </RichTextEditor.Toolbar> <RichTextEditor.Content /> </RichTextEditor>
Toggle dynamically:
Copy const [editable, setEditable] = useState(true); <button onClick={() => setEditable(!editable)}> {editable ? "Read-only" : "Edit"} </button> <RichTextEditor editor={editor} editable={editable}> ... </RichTextEditor>
BlockEditor
Use editor.setEditable() directly. The drag handle and bubble menu auto-hide when editor.isEditable is false:
Copy const editor = useEditor({ editable: false, extensions: [StarterKit, SlashCommand], }); <button onClick={() => { editor.setEditable(!editor.isEditable); }}> Toggle edit </button> <BlockEditor editor={editor} />
Custom Label Overrides
Customize button tooltips and aria-labels via the labels prop. All 33 labels can be overridden.
Copy <RichTextEditor editor={editor} labels={{ boldControlLabel: "Bold (B)", italicControlLabel: "italic (I)", linkControlLabel: "🔗 Link", h1ControlLabel: "Title", bulletListControlLabel: "List", }} > ... </RichTextEditor>
Custom Icon Overrides
Replace any built-in toolbar icon via the icons prop. All 27 icons accept a React node, so you can use inline SVGs, emoji, or any icon library.
Copy <RichTextEditor editor={editor} icons={{ boldControlIcon: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className="rte-editor-icon"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z" /><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z" /></svg>, italicControlIcon: <em className="not-italic text-base font-serif rte-editor-icon leading-none">I</em>, linkControlIcon: "🔗", }} > ... </RichTextEditor>
The rte-editor-icon class controls icon sizing in the toolbar. Apply it to your custom icons so they inherit the correct dimensions:
Copy .rte-editor-icon { width: 1rem; height: 1rem; }
See the Styling docs for more on sizing.
BlockEditor
Default
The BlockEditor ships as a single variant. Use className and CSS overrides for custom sizing, borders, and spacing.
Scoped CSS Variables
Same as the editor — wrap in a div with overridden shadcn tokens to theme a specific instance.
Copy <div style={{ "--primary": "oklch(0.55 0.25 280)", "--radius": "0.5rem", "--accent": "oklch(0.9 0.06 280)", }}> <BlockEditor editor={editor} /> </div>
Root className
Apply Tailwind classes to the root container for borders, shadows, and layout.
Copy <BlockEditor editor={editor} className="border-2 border-dashed border-primary/50 rounded-xl" />
Custom Slash Commands
Add your own items to the slash command (/) menu by passing custom items to getSlashCommandSuggestion.
Copy import { SlashCommand, defaultSlashCommandItems, getSlashCommandSuggestion, } from "@editorcn/block-editor"; import type { SlashCommandSuggestionItem } from "@editorcn/block-editor"; const customItems: SlashCommandSuggestionItem[] = [ { id: "greeting", title: "Greeting", description: "Insert a friendly greeting.", keywords: ["hello", "hi", "greeting"], command: ({ editor, range }) => { editor.chain().focus().deleteRange(range) .insertContent("Hello there! 👋").run(); }, }, { id: "image", title: "Image", description: "Insert an image.", keywords: ["image", "img", "picture"], command: ({ editor, range }) => { const url = window.prompt("Enter image URL"); // Replace with your own dialog/modal if (!url) return; editor.chain().focus().deleteRange(range) .setImage({ src: url }).run(); }, }, ]; SlashCommand.configure({ suggestion: getSlashCommandSuggestion(customItems), });
Custom items appear at the top of the menu. Built-in items with the same id are replaced; all others are appended below your custom items.