/
233
Sponsor

BlockEditor

Notion-style block-based rich text editor with slash commands and bubble menu.

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

PropTypeDefaultDescription
editorEditor | nullThe Tiptap editor instance
childrenReactNodeOptional custom children to replace the default layout
classNamestringAdditional CSS classes
labelsPartial<BlockEditorLabels>DEFAULT_BLOCK_EDITOR_LABELSOverride default slash command labels
iconsPartial<BlockEditorIcons>DEFAULT_ICONSOverride 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

KeyDefaultDescription
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:

CommandDescription
TextPlain paragraph
Heading 1/2/3Section headings
Bullet ListUnordered list
Numbered ListOrdered list
Task ListChecklist with checkboxes
QuoteBlockquote
CodeCode block with syntax highlighting (requires lowlight)
DividerHorizontal 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:

ExportDescription
DEFAULT_ICONSDefault toolbar, slash menu, and bubble menu icons
DEFAULT_LANGUAGE_ICONSPer-language icons for 21 languages (js, ts, python, rust, go, html, css, etc.)

Available icon keys:

KeyUsed in
slashTextIconSlash menu
slashHeadingIconSlash menu
slashBulletListIconSlash menu
slashOrderedListIconSlash menu
slashTaskListIconSlash menu
slashBlockquoteIconSlash menu
slashCodeBlockIconSlash menu
slashDividerIconSlash menu
slashImageIconSlash menu
slashTableIconSlash menu
searchIconSlash menu
fallbackIconSlash menu
dragHandleIconDrag handle
boldIconBubble menu
italicIconBubble menu
underlineIconBubble menu
strikethroughIconBubble menu
codeIconBubble menu
alignLeftIconBubble menu
alignCenterIconBubble menu
alignRightIconBubble menu
linkIconBubble menu
unlinkIconBubble menu
checkIconBubble menu
copyIconBubble menu
deleteIconBubble menu
dropdownArrowIconBubble menu
languageIconsCode 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.

ExtensionPurposeRequired
@tiptap/starter-kitCore editor functionalityYes
@tiptap/pmProseMirror runtimeYes
@tiptap/reactReact bindingsYes
@tiptap/suggestionSlash command suggestion engineYes
@tiptap/extension-drag-handle-reactBlock drag handle UIYes
@tiptap/extension-code-block-lowlightSyntax highlighting (requires lowlight)Optional
@tiptap/extension-placeholderPlaceholder textOptional

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.