Rtecn

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.

VariantLookBest for
defaultBordered container with classic joined segmented toolbar buttonsStandard editor experiences where the toolbar should feel distinct from content
subtleNo visible border — uses a soft shadow. Muted, rounded toolbar buttons. Elevated card feel.Forms, settings pages, or anywhere a less prominent editor fits
compactMuted 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.

On this page