Installation
Install via the shadcn registry (recommended):
$ pnpm dlx shadcn@latest add @editorcn/block-editor
This copies the component source directly into your project, so you can customize it freely.
Or install via npm/pnpm:
$ pnpm add @editorcn/block-editor @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/suggestion @tiptap/extension-drag-handle-react lucide-react
After installation, import the editor styles at the root of your application:
// Your shadcn globals
import "@/app/globals.css";
// Block editor component styles
import "@/components/block-editor/style.css";Order matters: Import your shadcn globals first, then the editor styles.
If you installed via npm, import from @editorcn/block-editor instead of @/components/block-editor throughout this page:
import {
BlockEditor,
SlashCommand,
defaultSlashCommandItems,
getSlashCommandSuggestion,
DEFAULT_ICONS,
} from "@/components/block-editor";
import type { BlockEditorIcons } from "@/components/block-editor";
import "@editorcn/block-editor/style.css";Install Extensions
For demo, we will use these extensions:
$ pnpm 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-header
Usage
"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 "@/components/block-editor";
import type { SlashCommandSuggestionItem } from "@/components/block-editor";
import "@/components/block-editor/style.css";
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 |
icons | Partial<BlockEditorIcons> | DEFAULT_ICONS | Override any built-in icon (slash commands, bubble menu, drag handle) |
Icons
By default all icons come from lucide-react, which inherits your theme's colors automatically (they render with currentColor). The block editor declares lucide-react as a dependency, so it is installed automatically via the shadcn registry or alongside the npm package.
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 "@/components/block-editor";
import type {
SlashCommandSuggestionItem,
OnCommandSelect,
} from "@/components/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 icons
All default icons come from lucide-react. Override any built-in icon via the icons prop. All icons accept a ReactNode, so you can use inline SVGs, emoji, or any icon library.
import { BlockEditor, DEFAULT_ICONS } from "@/components/block-editor";
import type { BlockEditorIcons } from "@/components/block-editor";
<BlockEditor
editor={editor}
icons={{
slashTextIcon: <span>T</span>,
slashHeadingIcon: <span>H</span>,
boldIcon: <strong>B</strong>,
dragHandleIcon: <span>:::</span>,
}}
/>;Each slash command item also accepts an icon property. Override individual items to change their icon without replacing all defaults:
import {
defaultSlashCommandItems,
getSlashCommandSuggestion,
} from "@/components/block-editor";
import type { SlashCommandSuggestionItem } from "@/components/block-editor";
const myItems: SlashCommandSuggestionItem[] = [
...defaultSlashCommandItems,
{
id: "image",
title: "Image",
description: "Insert an image.",
keywords: ["image", "img", "picture", "photo"],
icon: <ImageIcon />, // Your custom icon
command: ({ editor, range }) => {
const url = window.prompt("Enter image URL");
if (!url) return;
editor.chain().focus().deleteRange(range).setImage({ src: url }).run();
},
},
];Language icons
The code block language selector shows the language name (properly capitalized, e.g. TypeScript, C++, JSON) alongside its icon. Override per-language icons via the languageIcons key:
import {
BlockEditor,
DEFAULT_ICONS,
DEFAULT_LANGUAGE_ICONS,
} from "@/components/block-editor";
<BlockEditor
editor={editor}
icons={{
...DEFAULT_ICONS,
languageIcons: {
...DEFAULT_LANGUAGE_ICONS,
jsx: <YourJsxIcon />,
},
}}
/>;Exported constants:
| Export | Description |
|---|---|
DEFAULT_ICONS | Default toolbar, slash menu, and bubble menu icons |
DEFAULT_LANGUAGE_ICONS | Per-language icons for 21 languages (js, ts, python, rust, go, html, css, etc.) |
Available icon keys:
| Key | Used in |
|---|---|
slashTextIcon | Slash menu |
slashHeadingIcon | Slash menu |
slashBulletListIcon | Slash menu |
slashOrderedListIcon | Slash menu |
slashTaskListIcon | Slash menu |
slashBlockquoteIcon | Slash menu |
slashCodeBlockIcon | Slash menu |
slashDividerIcon | Slash menu |
slashImageIcon | Slash menu |
slashTableIcon | Slash menu |
searchIcon | Slash menu |
fallbackIcon | Slash menu |
dragHandleIcon | Drag handle |
boldIcon | Bubble menu |
italicIcon | Bubble menu |
underlineIcon | Bubble menu |
strikethroughIcon | Bubble menu |
codeIcon | Bubble menu |
alignLeftIcon | Bubble menu |
alignCenterIcon | Bubble menu |
alignRightIcon | Bubble menu |
linkIcon | Bubble menu |
unlinkIcon | Bubble menu |
checkIcon | Bubble menu |
copyIcon | Bubble menu |
deleteIcon | Bubble menu |
dropdownArrowIcon | Bubble menu |
languageIcons | Code block language selector (Record of language → icon) |
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. (with icons)
- Inline formatting — Bold, Italic, Underline, Strike, Code (with icons)
- Link insertion/removal — Add or remove links
- Text alignment — Left, Center, Right (with icons)
The bubble menu automatically positions itself near the selection and is styled with your theme's --popover tokens. All bubble menu icons can be overridden via the icons prop.
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 (shows a checkmark animation on success)
- 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:
$ pnpm add lowlight @tiptap/extension-code-block-lowlight
import 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:
$ pnpm add @tiptap/extension-placeholder
import 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.