Rtecn
@rtecn/block-editor

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-editor
yarn dlx shadcn@latest add @rtecn/block-editor
pnpm dlx shadcn@latest add @rtecn/block-editor
bunx shadcn@latest add @rtecn/block-editor

This 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-react
yarn add @rtecn/block-editor @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/suggestion @tiptap/extension-drag-handle-react
pnpm add @rtecn/block-editor @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/suggestion @tiptap/extension-drag-handle-react
bun add @rtecn/block-editor @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/suggestion @tiptap/extension-drag-handle-react

After 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-header
yarn 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
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
bun 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 "@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

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

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 "@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.

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:

npm install lowlight @tiptap/extension-code-block-lowlight
yarn add lowlight @tiptap/extension-code-block-lowlight
pnpm add lowlight @tiptap/extension-code-block-lowlight
bun 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:

npm install @tiptap/extension-placeholder
yarn add @tiptap/extension-placeholder
pnpm add @tiptap/extension-placeholder
bun 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.

On this page