BlockEditor
Notion-style block-based rich text editor with slash commands and bubble menu.
Block Editor
Package: @rtecn/block-editor
Import: import { BlockEditor } from "@rtecn/block-editor"
Description: Notion-style block-based editor with slash commands, drag handles, and a context-aware bubble menu for inline formatting.
Installation
Install via the shadcn registry (recommended):
npx shadcn@latest add @rtecn/block-editoryarn dlx shadcn@latest add @rtecn/block-editorpnpm dlx shadcn@latest add @rtecn/block-editorbunx shadcn@latest add @rtecn/block-editorThis copies the component source directly into your project, so you can customize it freely.
Or install via npm/pnpm:
npm install @rtecn/block-editor @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/suggestion @tiptap/extension-drag-handle-reactyarn add @rtecn/block-editor @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/suggestion @tiptap/extension-drag-handle-reactpnpm add @rtecn/block-editor @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/suggestion @tiptap/extension-drag-handle-reactbun add @rtecn/block-editor @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/suggestion @tiptap/extension-drag-handle-reactAfter installation, import the editor styles at the root of your application:
// Your shadcn globals
import "@rtecn/ui/globals.css";
// Block editor styles
import "@rtecn/block-editor/style.css";If you installed via the shadcn registry, the package is copied into your project, so import from your local components path instead of @rtecn/block-editor throughout this page:
import {
BlockEditor,
SlashCommand,
defaultSlashCommandItems,
getSlashCommandSuggestion,
} from "@/components/rte-block-editor";
import "@/components/rte-block-editor/style.css";Install Extensions
For demo, we will use these extensions:
npm install @tiptap/extension-placeholder @tiptap/extension-underline @tiptap/extension-task-item @tiptap/extension-task-list @tiptap/extension-image @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-cell @tiptap/extension-table-headeryarn add @tiptap/extension-placeholder @tiptap/extension-underline @tiptap/extension-task-item @tiptap/extension-task-list @tiptap/extension-image @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-cell @tiptap/extension-table-headerpnpm add @tiptap/extension-placeholder @tiptap/extension-underline @tiptap/extension-task-item @tiptap/extension-task-list @tiptap/extension-image @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-cell @tiptap/extension-table-headerbun add @tiptap/extension-placeholder @tiptap/extension-underline @tiptap/extension-task-item @tiptap/extension-task-list @tiptap/extension-image @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-cell @tiptap/extension-table-headerUsage
"use client";
import { useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import Underline from "@tiptap/extension-underline";
import TaskList from "@tiptap/extension-task-list";
import TaskItem from "@tiptap/extension-task-item";
import Image from "@tiptap/extension-image";
import Table from "@tiptap/extension-table";
import TableRow from "@tiptap/extension-table-row";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
import {
BlockEditor,
SlashCommand,
defaultSlashCommandItems,
getSlashCommandSuggestion,
} from "@rtecn/block-editor";
import type { SlashCommandSuggestionItem } from "@rtecn/block-editor";
const myItems: SlashCommandSuggestionItem[] = [
...defaultSlashCommandItems,
{
id: "image",
title: "Image",
description: "Insert an image.",
keywords: ["image", "img", "picture", "photo"],
command: ({ editor, range }) => {
const url = window.prompt("Enter image URL"); // Replace with your own dialog component
if (!url) return;
editor.chain().focus().deleteRange(range).setImage({ src: url }).run();
},
},
{
id: "table",
title: "Table",
description: "Insert a table.",
keywords: ["table", "grid"],
command: ({ editor, range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run();
},
},
];
function MyBlockEditor() {
const editor = useEditor({
immediatelyRender: false,
extensions: [
StarterKit.configure({
heading: { levels: [1, 2, 3] },
}),
Placeholder.configure({ placeholder: "Type / for commands..." }),
Underline,
TaskList,
TaskItem.configure({ nested: true }),
Table,
TableRow,
TableCell,
TableHeader,
Image,
SlashCommand.configure({
suggestion: getSlashCommandSuggestion(myItems),
}),
],
});
return <BlockEditor editor={editor} />;
}BlockEditor (Root)
The root wrapper that provides editor context. Renders the full editor UI by default (drag handle, block actions, bubble menu, and content area) or accepts custom children.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | — | The Tiptap editor instance |
children | ReactNode | — | Optional custom children to replace the default layout |
className | string | — | Additional CSS classes |
labels | Partial<BlockEditorLabels> | DEFAULT_BLOCK_EDITOR_LABELS | Override default slash command labels |
Labels
| Key | Default | Description |
|---|---|---|
paragraphLabel | "Text" | Label for paragraph node |
headingLabel | "Heading" | Label for heading nodes |
bulletListLabel | "Bullet list" | Label for bullet list |
orderedListLabel | "Numbered list" | Label for ordered list |
taskListLabel | "To-do list" | Label for task list |
blockquoteLabel | "Quote" | Label for blockquote |
codeBlockLabel | "Code" | Label for code block |
dividerLabel | "Divider" | Label for divider |
BlockEditor.Content
Renders the editor content area with the drag handle and block actions. Use this when providing custom children to the root.
<BlockEditor editor={editor}>
<BlockEditor.Content />
</BlockEditor>BlockEditor.BubbleMenu
A standalone bubble menu component that can be used outside the BlockEditor root. Useful if you want to customize the bubble menu position or behavior.
<BlockEditor editor={editor}>
<BlockEditor.Content />
<BlockEditor.BubbleMenu />
</BlockEditor>Features
Slash command menu
Type / to open the slash command menu. The menu shows available block types. Navigate with arrow keys and press Enter to insert.
Available commands:
| Command | Description |
|---|---|
| Text | Plain paragraph |
| Heading 1/2/3 | Section headings |
| Bullet List | Unordered list |
| Numbered List | Ordered list |
| Task List | Checklist with checkboxes |
| Quote | Blockquote |
| Code | Code block with syntax highlighting (requires lowlight) |
| Divider | Horizontal rule |
Custom slash command items
import {
BlockEditor,
SlashCommand,
defaultSlashCommandItems,
getSlashCommandSuggestion,
} from "@rtecn/block-editor";
import type {
SlashCommandSuggestionItem,
OnCommandSelect,
} from "@rtecn/block-editor";
const myItems: SlashCommandSuggestionItem[] = [
...defaultSlashCommandItems,
{
id: "custom",
title: "Custom",
description: "A custom command",
keywords: ["custom"],
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).insertContent("Hello!").run();
},
},
];Pass the custom items array to getSlashCommandSuggestion(myItems). Items with the same id as a default item will override it.
Custom items are commonly used for commands that require additional extensions, such as Image and Table:
import Image from "@tiptap/extension-image";
import Table from "@tiptap/extension-table";
import TableRow from "@tiptap/extension-table-row";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
const myItems: SlashCommandSuggestionItem[] = [
...defaultSlashCommandItems,
{
id: "image",
title: "Image",
description: "Insert an image.",
keywords: ["image", "img", "picture", "photo"],
command: ({ editor, range }) => {
const url = window.prompt("Enter image URL"); // Replace with your own dialog component
if (!url) return;
editor.chain().focus().deleteRange(range).setImage({ src: url }).run();
},
},
{
id: "table",
title: "Table",
description: "Insert a table.",
keywords: ["table", "grid"],
command: ({ editor, range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run();
},
},
];
const editor = useEditor({
extensions: [
StarterKit,
Table,
TableRow,
TableCell,
TableHeader,
Image,
SlashCommand.configure({
suggestion: getSlashCommandSuggestion(myItems),
}),
],
});Bubble menu
Select text to see the floating bubble menu with:
- Node type selector — Switch between paragraph, headings, lists, etc.
- Inline formatting — Bold, Italic, Underline, Strike, Code
- Link insertion/removal — Add or remove links
- Text alignment — Left, Center, Right
The bubble menu automatically positions itself near the selection and is styled with your theme's --popover tokens.
Block actions
Click the drag handle (grip icon) on the left side of any block to access a dropdown with:
- Copy — Copy the current block content to clipboard
- Delete — Delete the current block
Drag handle
Drag blocks to reorder them using the grip icon on the left. The drag handle uses @tiptap/extension-drag-handle-react.
Extensions
All @tiptap/* packages are peer dependencies — you must install them in your project. This ensures there is only one copy of Tiptap's types, avoiding "duplicate instance" TypeScript errors. If you installed via the shadcn registry, the CLI will prompt you to add any missing peer dependencies automatically.
| Extension | Purpose | Required |
|---|---|---|
@tiptap/starter-kit | Core editor functionality | Yes |
@tiptap/pm | ProseMirror runtime | Yes |
@tiptap/react | React bindings | Yes |
@tiptap/suggestion | Slash command suggestion engine | Yes |
@tiptap/extension-drag-handle-react | Block drag handle UI | Yes |
@tiptap/extension-code-block-lowlight | Syntax highlighting (requires lowlight) | Optional |
@tiptap/extension-placeholder | Placeholder text | Optional |
Extensions for Image, Table, Link, and others are not included — add them as needed for your custom slash commands.
Syntax highlighting
To enable syntax highlighting in code blocks, install lowlight and pass it to @tiptap/extension-code-block-lowlight:
npm install lowlight @tiptap/extension-code-block-lowlightyarn add lowlight @tiptap/extension-code-block-lowlightpnpm add lowlight @tiptap/extension-code-block-lowlightbun add lowlight @tiptap/extension-code-block-lowlightimport CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import { common, createLowlight } from "lowlight";
const lowlight = createLowlight(common);
const editor = useEditor({
extensions: [
StarterKit.configure({ codeBlock: false }),
CodeBlockLowlight.configure({ lowlight }),
SlashCommand.configure({
suggestion: getSlashCommandSuggestion(),
}),
],
});Placeholder
Install @tiptap/extension-placeholder to show placeholder text:
npm install @tiptap/extension-placeholderyarn add @tiptap/extension-placeholderpnpm add @tiptap/extension-placeholderbun add @tiptap/extension-placeholderimport Placeholder from "@tiptap/extension-placeholder";
const editor = useEditor({
extensions: [
StarterKit,
Placeholder.configure({ placeholder: "Type / for commands..." }),
],
content: "",
});Styling
Both the editor UI and content area are styled using CSS variables from your shadcn theme. See the Styling guide for details on customization.