Tables
On this page
Tables are built from three parts: the model and commands in @better-rich-text/plugin-table, the table view the plugin contributes to EditorView, and the table interface in @better-rich-text/ui (table-ui.ts and src/table/). Everything a pointer can do has a command and a keyboard path, and every change is one undo step.
Styles
A table's look is a set of attributes on the table node:
| Attribute | Values |
|---|---|
styleName |
Style token, usually a preset id. Written as the class brt-table-<name>. |
headerRow, firstColumn, totalRow |
Emphasis for the first row, first column and last row. |
bandedRows, bandedColumns |
Alternate shading. Header, first column and totals are left out of the bands. |
borders |
all, outer, inner, horizontal or none. |
density |
compact, normal or roomy cell padding. |
accent |
#rrggbb. Fills the header and tints bands and borders. Without it the palette is neutral. |
Presets set all of these at once: Plain, Grid, Banded rows, Banded columns, Ledger, Minimal lines and Boxed, plus the colour schemes Ocean, Forest, Ember, Plum and Slate. table.setStyle applies a preset and any overrides in one step:
editor.execute('table.setStyle', { style: 'ledger', accent: '#2e6b47', density: 'roomy' });A table that has a totals row keeps its total emphasis when you pick another preset.
The presentation of each cell is computed by presentTable(table, mode). In editor mode, colours are mixed against the skin's --brt-canvas and --brt-text tokens, so styles follow light, dark and high-contrast themes. In export mode they are concrete #rrggbb values. Shared cell edges are resolved once for the whole grid, so both cells on an edge agree and explicit borders always win.
Tables without any appearance attribute (including older documents that only have styleName) keep their previous output exactly.
Cell formatting
Shading:
table.setCellBackground { color }on the cell selection or the current cell. Text on a dark fill turns white; in the editor, light fills always get dark text so they stay readable in dark themes.Borders:
table.setCellBorderswithtop,right,bottom,left(true, false or null to follow the style), acolor, or apreset(all,outer,inner,none,reset). Single sides apply to that edge of the selection, as in word processors.Alignment:
table.setCellAttributes { align, valign }.table.clearCellFormattingremoves shading, alignment and borders.
Structure
| Task | Command | Keys |
|---|---|---|
| Insert rows or columns | table.addRowBefore/After, table.addColumnBefore/After with count |
|
| Duplicate | table.duplicateRow, table.duplicateColumn |
|
| Move | table.moveRow, table.moveColumn |
Mod-Alt-Arrow |
| Merge | table.mergeCells |
|
| Split | table.splitCell { rows, cols } |
|
| Unmerge | table.splitCell without input |
|
| Select | table.selectRow, table.selectColumn, table.selectTable |
Escape leaves a cell selection |
| Delete | table.deleteRow, table.deleteColumn, table.delete |
Backspace or Delete clears a cell selection |
Splitting a cell into more rows or columns than it spans adds grid lines, and the cells beside it grow across them. Content stays in the top-left piece. Moves and duplicates refuse to cut through merged cells and say which cell is in the way.
Column widths
Drag a column border in the editor. The new width is written once, on release, with table.setColumnWidths, so a drag is one undo step. Double-click a border to return that column to automatic width. The keyboard alternative is the column width dialog (table.setColumnWidth { width, unit: 'px' | '%' }) and table.distributeColumns. Widths are stored in pixels per column and exported as <colgroup>. A percentage is converted using the table's pixel width, or the rendered width the interface passes as referenceWidth. Layout is auto (columns grow with content) or fixed.
Handles
Hovering a table shows grips on its top and left edges, a corner grip, and add-row and add-column buttons. A grip selects its row or column and dispatches a brt-table-handle event; the interface answers with a menu for that row or column (insert, duplicate, move, sort, shade, delete). None of this enters the document: the overlay sits outside the content element.
Spreadsheet paste
With the caret in a cell, pasting tab-separated text, comma-separated text (two or more lines with the same number of fields) or a table fills cells instead of nesting a table. This goes through the clipboard.pasteText and clipboard.pasteSlice filters into table.pasteGrid.
From one cell the table grows to fit, within
maxRowsandmaxColumns.A larger selection is filled by repeating the source when it is a whole multiple, and the source is clipped otherwise. Pass
fitto choose.A growing paste inserts rows above a totals row instead of overwriting it, and totals are recalculated.
Merged cells in the way, or a paste over the limits, fail without changing anything.
Pasted tables keep their formatting inside cells. Formula text such as
=SUM(A1:A3)stays text.
Copying a cell selection puts only those cells on the clipboard, as a table (formatting kept) and as tab-separated text for spreadsheets. Cutting clears the cells. Plugins can do the same for their own selections with the clipboard.selectionSlice filter.
Totals
table.insertTotalsRow { fn } adds a row (or updates the existing one) with sum, average, count, min or max for every column that holds numbers, a label in the first column, and the totalRow emphasis. Numbers are read with the locale-aware parser used for sorting: grouping and decimal separators, currency symbols and codes, accounting parentheses and percentages. Results are written in the column's own format.
The value is plain text with a formula attribute (data-formula in HTML). Totals update as you edit the table, in the same undo step; typing into a totals cell turns it into plain text. Set config: { table: { liveTotals: false } } to refresh only through table.recalculate. Only these fixed aggregates are evaluated; imported formulas are never executed.
Captions
The caption is an editable tableCaption node, the table's optional first child. It holds inline content, so it can be bold or linked. table.toggleCaption adds or removes it, Enter in the caption moves to the first cell, and table.setProperties { caption } sets plain text. Documents that stored the caption in the old caption attribute are migrated by a normaliser on load, with ids derived from the table id, and the migration is not an undo step.
Sorting
The sort dialog lists the columns by their first-row text, detects the type from the values (number, then currency, then date, else text), and shows the new order before applying, with the rows that move and the values that cannot be read. Totals rows stay at the bottom. The same plan is available without the dialog through the table service: previewSort(state, request).
HTML export and import
Exported tables carry classes for their style and, through the editor's codec, inline CSS on the table and every cell (padding, fills, text colour, weight, borders, alignment defaults). The HTML looks the same in email clients and CMSs that never load the editor's stylesheet. Copying a table puts the same HTML on the clipboard.
On import the classes restore the style attributes. Inline declarations that only repeat what the style draws are dropped, so cells keep following the style after a round trip. Explicit cell shading and borders written by other editors are read as cell formatting.
Interface
@better-rich-text/ui registers these toolbar items:
| Item | What it does |
|---|---|
table |
Insert dialog: live style gallery, size picker with arrow keys and exact numbers, header row and column, caption. |
tableMenu |
The full table menu. It also starts the table bar for the editor. |
tableStyle |
Style panel: gallery, emphasis and banding options, borders, spacing, accent colour. Changes apply live. |
tableShading |
Cell shading: palette, recent colours, custom value, native picker and the screen colour dropper. |
The table bar appears above the table holding the caret: style, shading, cell borders and alignment, row and column insertion, merge, split, sort, totals, the full menu and delete. It never takes focus from the editor on click.
The screen colour dropper uses the EyeDropper API. It is feature-detected and the button is absent in browsers without it.
Keyboard: Shift+F10 or the context-menu key inside a table opens the full table menu at the caret, which reaches every action. Menus, galleries and swatches use arrow keys. Structural changes are announced in a polite live region ("Row inserted", "Column 3 selected", "Table sorted").
Interface text is in tableMessages; replace entries with setTableMessages.
Accessibility
Header rows get scope="col", header columns scope="row", and the corner cell heads its column. New rows and columns copy the header type of their neighbours. The table overlay is hidden from assistive technology; every handle action has a menu entry and a command.
Not done yet
Fragment-preserving merges (Unmerge returning each cell's own content) need stable row and column ids in the model.
Repeat-header settings for paginated export and minimum row heights.