Customization
See how to customize both editors through variants, CSS variables, className props, and more.
Customization
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
<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.
<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.
<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.
<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.
import { useRichTextEditorContext } from "@rtecn/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:
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:
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:
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.
<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.
<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:
.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.
<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.
<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.
import {
SlashCommand,
defaultSlashCommandItems,
getSlashCommandSuggestion,
} from "@rtecn/block-editor";
import type { SlashCommandSuggestionItem } from "@rtecn/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.