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.
Live Playground
Section titled “Live Playground”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.
Vanilla JS preview - Angular components produce the same output
Vanilla JS preview - React components produce the same output
Plain table without the built-in TableView (configured with View: null). The buttons above the editor are custom HTML buttons wired to table commands like insertTable(), addRowAfter(), mergeCells(), etc.
Install @domternal/extension-table alongside the core package:
pnpm add @domternal/extension-tableimport { 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 buttonconst 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.
import { Component, signal } from '@angular/core';import { DomternalEditorComponent, DomternalToolbarComponent } from '@domternal/angular';import { Editor, Document, Paragraph, Text } from '@domternal/core';import { Table } from '@domternal/extension-table';
@Component({ selector: 'app-editor', imports: [DomternalEditorComponent, DomternalToolbarComponent], templateUrl: './editor.html',})export class EditorComponent { editor = signal<Editor | null>(null); extensions = [Document, Paragraph, Text, Table]; content = '<p>Click the table button in the toolbar to insert a table.</p>';}@if (editor(); as ed) { <domternal-toolbar [editor]="ed" />}<domternal-editor [extensions]="extensions" [content]="content" (editorCreated)="editor.set($event)"/>The toolbar component auto-renders the table insert button. Once a table is created, cell handles and resize controls appear automatically.
import { Domternal } from '@domternal/react';import { Document, Paragraph, Text } from '@domternal/core';import { Table } from '@domternal/extension-table';
export default function Editor() { return ( <Domternal extensions={[Document, Paragraph, Text, Table]} content="<p>Click the table button in the toolbar to insert a table.</p>" > <Domternal.Toolbar /> <Domternal.Content /> </Domternal> );}The <Domternal.Toolbar /> component auto-renders the table insert button. Once a table is created, cell handles and resize controls appear automatically.
import { Editor, Document, Paragraph, Text } from '@domternal/core';import { Table } from '@domternal/extension-table';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [Document, Paragraph, Text, Table],});
// Insert a 3x3 table with a header roweditor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
// Add rows and columnseditor.commands.addRowAfter();editor.commands.addColumnAfter();
// Merge/split cells (requires cell selection)editor.commands.mergeCells();editor.commands.splitCell();
// Delete the tableeditor.commands.deleteTable();Schema
Section titled “Schema”The table extension defines four node types:
| Property | Value |
|---|---|
| ProseMirror name | table |
| Type | Node |
| Group | block |
| Content | tableRow+ (one or more table rows) |
| Isolating | Yes |
| HTML tag | <table> |
TableRow
Section titled “TableRow”| Property | Value |
|---|---|
| ProseMirror name | tableRow |
| Type | Node |
| Content | (tableCell | tableHeader)* |
| HTML tag | <tr> |
TableCell
Section titled “TableCell”| Property | Value |
|---|---|
| ProseMirror name | tableCell |
| Type | Node |
| Content | block+ (one or more block nodes) |
| Isolating | Yes |
| HTML tag | <td> |
TableHeader
Section titled “TableHeader”| Property | Value |
|---|---|
| ProseMirror name | tableHeader |
| Type | Node |
| Content | block+ (one or more block nodes) |
| Isolating | Yes |
| HTML tag | <th> |
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
HTMLAttributes | Record<string, unknown> | {} | HTML attributes added to the <table> element |
cellMinWidth | number | 25 | Minimum cell width in pixels (floor when dragging) |
defaultCellMinWidth | number | 100 | Default width for columns without an explicit colwidth attribute |
resizeBehavior | 'neighbor' | 'independent' | 'redistribute' | 'neighbor' | Column resize behavior (see below) |
constrainToContainer | boolean | true | Prevent the table from exceeding its container width |
allowTableNodeSelection | boolean | false | Allow selecting the entire table as a node selection |
View | NodeView constructor | null | TableView | Custom NodeView constructor, or null to disable |
Resize behaviors
Section titled “Resize behaviors”import { Table } from '@domternal/extension-table';
// Google Docs style: adjacent column compensates, table width stays constantconst NeighborTable = Table.configure({ resizeBehavior: 'neighbor' });
// Only dragged column changes, table width grows/shrinksconst 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.
Container constraint
Section titled “Container constraint”import { Table } from '@domternal/extension-table';
// Prevent table from exceeding container width (default)const ConstrainedTable = Table.configure({ constrainToContainer: true });
// Allow table to overflow horizontallyconst 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.
Attributes
Section titled “Attributes”TableCell and TableHeader share the same attributes:
| Attribute | Type | Default | Rendered as | Description |
|---|---|---|---|---|
colspan | number | 1 | colspan HTML attribute | Number of columns a cell spans |
rowspan | number | 1 | rowspan HTML attribute | Number of rows a cell spans |
colwidth | number[] | null | null | data-colwidth | Column widths in pixels (set during resize) |
background | string | null | null | data-background + style | Cell background color (hex or rgb) |
textAlign | string | null | null | data-text-align | Horizontal text alignment (left, center, right) |
verticalAlign | string | null | null | data-vertical-align | Vertical text alignment (top, middle, bottom) |
Default values (1 for colspan/rowspan, null for others) are omitted from the rendered HTML.
Commands
Section titled “Commands”Table structure
Section titled “Table structure”| Command | Description |
|---|---|
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 roweditor.commands.insertTable();
// Insert a 4x5 table without a header roweditor.commands.insertTable({ rows: 4, cols: 5, withHeaderRow: false });
// Delete the current tableeditor.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.
Row operations
Section titled “Row operations”| Command | Description |
|---|---|
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();Column operations
Section titled “Column operations”| Command | Description |
|---|---|
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.
Cell merge and split
Section titled “Cell merge and split”| Command | Description |
|---|---|
mergeCells() | Merge selected cells into a single cell |
splitCell() | Split a previously merged cell back to individual cells |
// Merge the current cell selectioneditor.chain().focus().mergeCells().run();
// Split a merged celleditor.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.
Header toggles
Section titled “Header toggles”| Command | Description |
|---|---|
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();Cell attributes
Section titled “Cell attributes”| Command | Description |
|---|---|
setCellAttribute(name, value) | Set an attribute on selected cells |
// Set background coloreditor.chain().focus().setCellAttribute('background', '#ffe0e0').run();
// Set horizontal alignmenteditor.chain().focus().setCellAttribute('textAlign', 'center').run();
// Set vertical alignmenteditor.chain().focus().setCellAttribute('verticalAlign', 'middle').run();
// Remove background coloreditor.chain().focus().setCellAttribute('background', null).run();Navigation
Section titled “Navigation”| Command | Description |
|---|---|
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 });Keyboard shortcuts
Section titled “Keyboard shortcuts”| Shortcut | Action |
|---|---|
Tab | Move to the next cell. If at the last cell, a new row is added first |
Shift-Tab | Move to the previous cell |
Backspace | Delete the table if all cells are selected |
Delete | Delete the table if all cells are selected |
Mod-Backspace | Delete the table if all cells are selected |
Mod-Delete | Delete 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.
Toolbar items
Section titled “Toolbar items”Table registers a button in the toolbar with the name table in group insert at priority 140.
| Item | Command | Icon | Label |
|---|---|---|---|
| Insert Table | insertTable | table | Insert Table |
Cell toolbar
Section titled “Cell toolbar”When you select multiple cells (CellSelection), a floating cell toolbar appears above the selection with these controls:
| Button | Action |
|---|---|
| Color | Opens a 16-color palette to set cell background |
| Alignment | Opens a dropdown with horizontal (left, center, right) and vertical (top, middle, bottom) alignment |
| Merge | Merge selected cells (disabled if not possible) |
| Split | Split a merged cell (disabled if not possible) |
| Header | Toggle selected cells between header and data cells |
Row and column handles
Section titled “Row and column handles”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”
Column resizing
Section titled “Column resizing”Drag a column border to resize it. The resize behavior depends on the resizeBehavior option:
| Mode | Behavior | Table width |
|---|---|---|
neighbor | Adjacent column compensates | Stays constant |
independent | Only dragged column changes | Grows or shrinks |
redistribute | All columns adjust | Fills 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.
Included extensions
Section titled “Included extensions”Table automatically includes these nodes via addExtensions():
| Extension | Description |
|---|---|
| TableRow | The <tr> row container |
| TableCell | The <td> data cell with block content |
| TableHeader | The <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.
JSON representation
Section titled “JSON representation”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" }] } ] } ]}Source
Section titled “Source”@domternal/extension-table - Table.ts