Skip to content

Table

The Table extension provides a full-featured table editor built on prosemirror-tables. It includes cell selection, column resizing (three resize modes), merge/split cells, header row/column toggles, cell background colors, text alignment, and keyboard navigation. The extension ships as a separate package (@domternal/extension-table) and automatically includes the TableRow, TableCell, and TableHeader nodes.

Click inside the table and use Tab to navigate between cells.

With the default TableView enabled, hover over the table to see row and column handles, select multiple cells to reveal the cell toolbar, and drag column borders to resize.

Click to try it out

Install @domternal/extension-table alongside the core package:

Terminal window
pnpm add @domternal/extension-table
import { Editor, Document, Paragraph, Text, defaultIcons } from '@domternal/core';
import { Table } from '@domternal/extension-table';
import '@domternal/theme';
const editorEl = document.getElementById('editor')!;
// Toolbar with insert table button
const toolbar = document.createElement('div');
toolbar.className = 'dm-toolbar';
toolbar.innerHTML = `<div class="dm-toolbar-group">
<button class="dm-toolbar-button" id="insert-table">${defaultIcons.table}</button>
</div>`;
editorEl.before(toolbar);
const editor = new Editor({
element: editorEl,
extensions: [Document, Paragraph, Text, Table],
content: '<p>Click the table button to insert a table.</p>',
});
document.getElementById('insert-table')!.addEventListener('click', () => {
editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
});

Once inserted, the table provides built-in cell handles, column/row controls, and resize handles via @domternal/theme. Use Tab/Shift-Tab to navigate cells.

The table extension defines four node types:

PropertyValue
ProseMirror nametable
TypeNode
Groupblock
ContenttableRow+ (one or more table rows)
IsolatingYes
HTML tag<table>
PropertyValue
ProseMirror nametableRow
TypeNode
Content(tableCell | tableHeader)*
HTML tag<tr>
PropertyValue
ProseMirror nametableCell
TypeNode
Contentblock+ (one or more block nodes)
IsolatingYes
HTML tag<td>
PropertyValue
ProseMirror nametableHeader
TypeNode
Contentblock+ (one or more block nodes)
IsolatingYes
HTML tag<th>
OptionTypeDefaultDescription
HTMLAttributesRecord<string, unknown>{}HTML attributes added to the <table> element
cellMinWidthnumber25Minimum cell width in pixels (floor when dragging)
defaultCellMinWidthnumber100Default width for columns without an explicit colwidth attribute
resizeBehavior'neighbor' | 'independent' | 'redistribute''neighbor'Column resize behavior (see below)
constrainToContainerbooleantruePrevent the table from exceeding its container width
allowTableNodeSelectionbooleanfalseAllow selecting the entire table as a node selection
ViewNodeView constructor | nullTableViewCustom NodeView constructor, or null to disable
import { Table } from '@domternal/extension-table';
// Google Docs style: adjacent column compensates, table width stays constant
const NeighborTable = Table.configure({ resizeBehavior: 'neighbor' });
// Only dragged column changes, table width grows/shrinks
const IndependentTable = Table.configure({ resizeBehavior: 'independent' });
// All columns redistribute to fill available width (prosemirror-tables default)
const RedistributeTable = Table.configure({ resizeBehavior: 'redistribute' });

neighbor (default): When you drag a column border, the adjacent column compensates so the total table width stays constant. This is the behavior found in Google Docs.

independent: All columns freeze to their current width on the first resize. Only the dragged column changes width, and the table grows or shrinks accordingly.

redistribute: The original prosemirror-tables behavior. All columns redistribute to fill the available width.

import { Table } from '@domternal/extension-table';
// Prevent table from exceeding container width (default)
const ConstrainedTable = Table.configure({ constrainToContainer: true });
// Allow table to overflow horizontally
const UnconstrainedTable = Table.configure({ constrainToContainer: false });

When constrainToContainer is true (default), last-column resize is capped at the container edge and adding a column redistributes existing columns if the table would overflow. When false, the table can grow beyond its container and scroll horizontally.

TableCell and TableHeader share the same attributes:

AttributeTypeDefaultRendered asDescription
colspannumber1colspan HTML attributeNumber of columns a cell spans
rowspannumber1rowspan HTML attributeNumber of rows a cell spans
colwidthnumber[] | nullnulldata-colwidthColumn widths in pixels (set during resize)
backgroundstring | nullnulldata-background + styleCell background color (hex or rgb)
textAlignstring | nullnulldata-text-alignHorizontal text alignment (left, center, right)
verticalAlignstring | nullnulldata-vertical-alignVertical text alignment (top, middle, bottom)

Default values (1 for colspan/rowspan, null for others) are omitted from the rendered HTML.

CommandDescription
insertTable(options?)Insert a new table at the cursor position
deleteTable()Delete the entire table the cursor is in
fixTables()Repair malformed table structure
// Insert a default 3x3 table with a header row
editor.commands.insertTable();
// Insert a 4x5 table without a header row
editor.commands.insertTable({ rows: 4, cols: 5, withHeaderRow: false });
// Delete the current table
editor.chain().focus().deleteTable().run();

insertTable accepts an optional object with rows (default 3), cols (default 3), and withHeaderRow (default true). It returns false if the cursor is already inside a table or a code block.

CommandDescription
addRowBefore()Insert a row above the current row
addRowAfter()Insert a row below the current row
deleteRow()Delete the current row (deletes the table if it is the last row)
editor.chain().focus().addRowAfter().run();
editor.chain().focus().deleteRow().run();
CommandDescription
addColumnBefore()Insert a column to the left of the current column
addColumnAfter()Insert a column to the right of the current column
deleteColumn()Delete the current column (deletes the table if it is the last column)
editor.chain().focus().addColumnAfter().run();
editor.chain().focus().deleteColumn().run();

When constrainToContainer is enabled, adding a column redistributes existing column widths to prevent the table from overflowing its container.

CommandDescription
mergeCells()Merge selected cells into a single cell
splitCell()Split a previously merged cell back to individual cells
// Merge the current cell selection
editor.chain().focus().mergeCells().run();
// Split a merged cell
editor.chain().focus().splitCell().run();

mergeCells requires a CellSelection (multiple cells selected). splitCell works on the cell at the cursor if it has colspan or rowspan greater than 1. Both return false if the operation is not possible.

CommandDescription
toggleHeaderRow()Toggle the first row between header cells (<th>) and data cells (<td>)
toggleHeaderColumn()Toggle the first column between header cells and data cells
toggleHeaderCell()Toggle individual selected cells between header and data cells
editor.chain().focus().toggleHeaderRow().run();
CommandDescription
setCellAttribute(name, value)Set an attribute on selected cells
// Set background color
editor.chain().focus().setCellAttribute('background', '#ffe0e0').run();
// Set horizontal alignment
editor.chain().focus().setCellAttribute('textAlign', 'center').run();
// Set vertical alignment
editor.chain().focus().setCellAttribute('verticalAlign', 'middle').run();
// Remove background color
editor.chain().focus().setCellAttribute('background', null).run();
CommandDescription
goToNextCell()Move the cursor to the next cell
goToPreviousCell()Move the cursor to the previous cell
setCellSelection(position)Create a cell selection programmatically
editor.commands.goToNextCell();
editor.commands.goToPreviousCell();
// Programmatic cell selection (positions are ProseMirror document positions)
editor.commands.setCellSelection({ anchorCell: 5, headCell: 12 });
ShortcutAction
TabMove to the next cell. If at the last cell, a new row is added first
Shift-TabMove to the previous cell
BackspaceDelete the table if all cells are selected
DeleteDelete the table if all cells are selected
Mod-BackspaceDelete the table if all cells are selected
Mod-DeleteDelete the table if all cells are selected

Tab and Shift-Tab defer to list extensions when the cursor is inside a listItem or taskItem, so list indentation takes precedence over table navigation.

Table registers a button in the toolbar with the name table in group insert at priority 140.

ItemCommandIconLabel
Insert TableinsertTabletableInsert Table

When you select multiple cells (CellSelection), a floating cell toolbar appears above the selection with these controls:

ButtonAction
ColorOpens a 16-color palette to set cell background
AlignmentOpens a dropdown with horizontal (left, center, right) and vertical (top, middle, bottom) alignment
MergeMerge selected cells (disabled if not possible)
SplitSplit a merged cell (disabled if not possible)
HeaderToggle selected cells between header and data cells

When hovering over a table, interactive handles appear:

  • Column handle (horizontal dots above the table): Click to open a dropdown with “Insert Column Left”, “Insert Column Right”, and “Delete Column”
  • Row handle (vertical dots to the left of the table): Click to open a dropdown with “Insert Row Above”, “Insert Row Below”, and “Delete Row”

Drag a column border to resize it. The resize behavior depends on the resizeBehavior option:

ModeBehaviorTable width
neighborAdjacent column compensatesStays constant
independentOnly dragged column changesGrows or shrinks
redistributeAll columns adjustFills available width

The resize handle (a thin blue line) appears when hovering near a column border. During text selection or cell drag, the resize handle is suppressed to prevent accidental resizes.

Column widths are stored as colwidth attributes on cells and persist across serialization. The data-colwidth HTML attribute stores the values as a comma-separated list.

Table automatically includes these nodes via addExtensions():

ExtensionDescription
TableRowThe <tr> row container
TableCellThe <td> data cell with block content
TableHeaderThe <th> header cell with block content

This means adding Table to your extensions array is sufficient. You don’t need to add the row/cell nodes separately.

A simple table with headers:

{
"type": "table",
"content": [
{
"type": "tableRow",
"content": [
{
"type": "tableHeader",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Name" }]
}
]
},
{
"type": "tableHeader",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Role" }]
}
]
}
]
},
{
"type": "tableRow",
"content": [
{
"type": "tableCell",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Alice" }]
}
]
},
{
"type": "tableCell",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Engineer" }]
}
]
}
]
}
]
}

A cell with merged columns and a background color:

{
"type": "tableCell",
"attrs": {
"colspan": 2,
"rowspan": 1,
"colwidth": null,
"background": "#ffe0e0",
"textAlign": "center",
"verticalAlign": null
},
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Merged cell" }]
}
]
}

An empty table (3x3 with header row):

{
"type": "table",
"content": [
{
"type": "tableRow",
"content": [
{ "type": "tableHeader", "content": [{ "type": "paragraph" }] },
{ "type": "tableHeader", "content": [{ "type": "paragraph" }] },
{ "type": "tableHeader", "content": [{ "type": "paragraph" }] }
]
},
{
"type": "tableRow",
"content": [
{ "type": "tableCell", "content": [{ "type": "paragraph" }] },
{ "type": "tableCell", "content": [{ "type": "paragraph" }] },
{ "type": "tableCell", "content": [{ "type": "paragraph" }] }
]
},
{
"type": "tableRow",
"content": [
{ "type": "tableCell", "content": [{ "type": "paragraph" }] },
{ "type": "tableCell", "content": [{ "type": "paragraph" }] },
{ "type": "tableCell", "content": [{ "type": "paragraph" }] }
]
}
]
}

@domternal/extension-table - Table.ts