The Table extension extends Tiptap's Table with a TableToolbar that inserts a table by dragging across a grid-size picker (like Notion), then manages it while the cursor is inside a table: add or delete rows and columns, merge and split cells, and toggle the header row. Columns are resizable by dragging their edges. Each column has a fixed default width (8rem), so adding a column extends the table and shows a horizontal scroll instead of shrinking existing columns.
While the cursor is inside a table, a hover overlay shows three-dot menu handles at the top of the hovered column and to the left of the hovered row. The handles detect hover over the entire .tableWrapper container, so they stay visible when the pointer is in the scroll area around the table. They are positioned from the editor's visible box, so they track the table while it scrolls sideways and are dropped as soon as the table, row or column scrolls out of view. Hover a handle to reveal its ⋮ menu, or click it to open that row's or column's actions, or use the toolbar control. The Gapcursor extension is bundled automatically, so you can click between blocks to move the cursor into or past a table with no extra setup.
Installation
Run the following command to install the extension, its toolbar component and the shared extension runtime:
$ pnpm dlx shadcn@latest add "https://editorcn.vercel.app/r/table.json"
This installs extensions/table, extensions/core/, extensions/ui/ and the @tiptap/extension-table* packages that the table node depends on. Table styles and shared UI styles are installed as extensions/table/style.css and extensions/ui/style.css.
To install the files manually instead, install these packages and copy the code from the package source:
$ pnpm add @tiptap/core @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-cell @tiptap/extension-table-header @tiptap/extension-gapcursor
@tiptap/extension-gapcursor is only needed if you register Table yourself — the extension includes Gapcursor by default.
Usage
Register Table in your editor and add TableToolbar to a toolbar group. The toolbar accepts an optional editor prop or falls back to an EditorControlsProvider. Place TableHoverOverlay inside the content area to enable the row/column context handles.
"use client";
import { useEditor } from "@tiptap/react";
import { StarterKit } from "@tiptap/starter-kit";
import { Table } from "@/components/extensions/table";
import { TableHoverOverlay } from "@/components/extensions/table/table-hover-overlay";
import { TableToolbar } from "@/components/extensions/table/toolbar";
import { RichTextEditor } from "@/components/editor";
import "@/components/editor/style.css";
import "@/components/extensions/table/style.css";
import "@/components/extensions/ui/style.css";
export function MyEditor() {
const editor = useEditor({
immediatelyRender: false,
extensions: [StarterKit, Table],
});
return (
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Undo />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<TableToolbar editor={editor} />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content>
<TableHoverOverlay editor={editor} />
</RichTextEditor.Content>
</RichTextEditor>
);
}With @editorcn/block-editor
The block editor has no toolbar, so insert the table from a slash command and manage it with the hover context handles that TableHoverOverlay renders when the cursor is inside a table.
"use client";
import { useEditor } from "@tiptap/react";
import { StarterKit } from "@tiptap/starter-kit";
import {
BlockEditor,
SlashCommand,
defaultSlashCommandItems,
getSlashCommandSuggestion,
} from "@/components/block-editor";
import type { SlashCommandSuggestionItem } from "@/components/block-editor";
import { Table } from "@/components/extensions/table";
import { TableHoverOverlay } from "@/components/extensions/table/table-hover-overlay";
import "@/components/block-editor/style.css";
import "@/components/extensions/table/style.css";
import "@/components/extensions/ui/style.css";
const tableItem: SlashCommandSuggestionItem = {
command: ({ editor, range }) =>
editor.chain().focus().deleteRange(range).insertTableWithHeader().run(),
description: "Insert a table with a header row",
id: "insertTableWithHeader",
keywords: ["table", "row", "column"],
title: "Insert table",
};
function MyBlockEditor() {
const editor = useEditor({
immediatelyRender: false,
extensions: [
StarterKit,
Table,
SlashCommand.configure({
suggestion: getSlashCommandSuggestion([
...defaultSlashCommandItems,
tableItem,
]),
}),
],
});
return (
<div className="relative">
<BlockEditor editor={editor} />
<TableHoverOverlay editor={editor} />
</div>
);
}Render TableHoverOverlay next to BlockEditor, inside a positioned (relative) container so the overlay anchors to the editor. The overlay measures its controls against the editor's visible box and clips them to it, so a table that scrolls out of view drops its handles instead of leaving them floating over the surrounding UI. Its hover handles on the hovered row and column are what add or delete rows and columns, merge or split cells, and toggle the header row — the TableToolbar control is only needed in toolbar editors.
Do not put overflow-hidden on that container: the editor's own bubble menu and drag handle are positioned inside it and would be clipped along with the table handles.
Settings
defaultCols
Default number of columns for a newly inserted table.
Table.configure({ defaultCols: 4 });defaultRows
Default number of rows for a newly inserted table.
Table.configure({ defaultRows: 5 });withHeaderRow
Inserts a header row with the table.
Table.configure({ withHeaderRow: false });resizable
Enables column resizing by dragging the right edge of each column. Defaults to true.
Table.configure({ resizable: false });HTMLAttributes
HTML attributes applied to the <table> element. All @tiptap/extension-table options are also supported.
Table.configure({
HTMLAttributes: { class: "w-full" },
});Commands
insertTableWithHeader
Inserts a table using defaultCols, defaultRows and withHeaderRow.
editor.commands.insertTableWithHeader();moveColumn
Moves the column at index from to index to (0-based, clamped), preserving column widths. Useful for building drag-and-drop or shift controls around the table.
editor.commands.moveColumn(0, 2);moveRow
Moves the row at index from to index to (0-based, clamped). The row index includes the header row.
editor.commands.moveRow(1, 3);The toolbar also exposes the underlying @tiptap/extension-table commands:
editor.commands.addRowBefore();
editor.commands.addRowAfter();
editor.commands.deleteRow();
editor.commands.addColumnBefore();
editor.commands.addColumnAfter();
editor.commands.deleteColumn();
editor.commands.toggleHeaderRow();
editor.commands.mergeCells();
editor.commands.splitCell();
editor.commands.deleteTable();Scaffold production-ready Python, Go, and Rust projects from one CLI. Open source, zero lock-in.