Tables

In the CMS a table is built with Insert Table and edited through the table menu, with Tab and Shift-Tab moving between cells. This page is the markdown underneath: how alignment, spans and cell classes are written, the form the editor writes them back in, and the limits on a table’s size.

Basic table

| Header 1 | Header 2 |
| -------- | -------- |
| Cell     | Cell     |
| Cell     | Cell     |

How it renders

Header 1Header 2
CellCell
CellCell

The first row is the header, the dashed row separates it from the body, and every row after that is data. The dashes do not have to line up with anything and the pipes do not have to be padded — |Header 1|Header 2| parses identically. Aligning the source is purely for whoever reads the markdown next.

Column alignment

Colons in the delimiter row set a column’s alignment. It is emitted as an inline style on every th and td in that column, so it survives into any stylesheet without needing a class. Alignment belongs to the column, not to a cell’s position in its row: a cell beside a span takes the alignment of the column it starts in.

| Left | Right | Center |
| :--- | ----: | :----: |
| data |  data |  data  |

How it renders

LeftRightCenter
datadatadata
MarkerAlignmentWritten back by the editor as
:---left:---------
---:right---------:
:---:center:--------:
---none----------

Formatting inside cells

Cells take inline formatting — marks, code, links and icons all work:

| **Bold** | *Italic* | `Code` | [Link](https://leed.ai) |
| -------- | -------- | ------ | ----------------------- |
| Data     | Data     | Data   | Data                    |

How it renders

BoldItalicCodeLink
DataDataDataData

What a cell cannot contain is a block: no paragraphs, no lists, no fenced code. A cell is one line of inline content, so a line break inside a cell is written as <br>, which is how the editor stores one, and a pipe inside it has to be escaped as \|. An image in a cell is dropped from a CMS page body, and validation reports it as dropped content.

Table attributes

Attributes for the whole table go on a line of their own after the last row. The editor writes that line after a blank line, as below; a line directly under the last row, as older page bodies wrote it, is read the same way:

| Plan  | Price |
| ----- | ----- |
| Basic | $10   |

{class="pricing"}

Either way the line is recognized as nothing but attributes, applied to the <table> element and removed, so it never shows up as an extra row or a paragraph on the published page.

Every other entry on that line lands on the <table>, not only class and data-* — {role="grid"} reaches the element too. That is what the renderer does with markdown from a template; a CMS page body models a table’s data-id and nothing else, so a class written there is dropped on import.

Cell attributes

A single cell takes attributes at the end of its own content, and shorthand does work inside a cell. A cell keeps three things — colspan, rowspan and a class. Any other key in its group, such as style, align or a data-* attribute, is dropped and reported under dropped_attributes, and an id is dropped as it is everywhere:

| Item                         |   Q1 |   Q2 |
| :--------------------------- | ---: | ---: |
| Combined total {colspan="2"} |   41 |
| Rolling {rowspan="2"}        |   12 |   19 |
| 14                           |   22 |
| Regional {.muted}            |    8 |   11 |

How it renders

ItemQ1Q2
Combined total41
Rolling1219
1422
Regional811
  • {colspan="2"} makes the cell span two columns. A body row lists only the cells that start in it, so that row carries one fewer cell than the header.
  • The header row is read by position: a position a colspan covers holds an empty filler cell, which is dropped. That is how the editor writes a header cell that spans columns.
  • {rowspan="2"} makes the cell span down into the next row. That row lists only the cells that start in it, each placed in the next column the span leaves free. A cell written where no column is free — such as an empty filler under a rowspan, which older tables wrote — is dropped, so a table that writes one renders as it always did.
  • {.muted} puts a class on that one cell, and the editor writes it back as {class="muted"}. Cell attributes stay on the cell — they are not promoted to the table.
  • A span that overlaps another cell or runs past the table is cut to fit, and a span that is not a whole number of at least 1 counts as 1. Both are reported, and so is a cell with content that is dropped.
TargetWhere you write itShorthand?Lands on
Whole tableits own line, after the last rowNo — key=value entries onlythe <table>
One cellat the end of that cell’s contentYesthat <td> or <th>

The split between the attribute line and an element’s own group is explained on Attributes; it is the same rule that governs lists and blockquotes. A cell’s attributes stay on the cell and are never moved onto the table.

What the editor does to a table

Nothing in a table is lost on a save: alignment, spans and cell classes are written back, in the normalized form below. A header-less table loses nothing either, but is written in a form worth recognizing, and a few limits are worth knowing before you build something elaborate.

Alignment, spans and classes are written back

The serializer writes each column’s alignment into the delimiter row, taken from the topmost cell that starts in that column. A cell’s spans and class are written as one group at the end of its content — {colspan="2" rowspan="2" class="muted"} — and a position a span covers is not written, apart from the empty filler in the header row. The column count comes from the spans, so a merged cell in the first row no longer shortens the delimiter row.

Merge cells and Split cell

Merge cells in the table menu sets colspan and rowspan, and Split cell takes them away again. A table built with them saves and reloads with the same grid, and the published site renders the same grid the editor shows. Spans authored in markdown open in the editor as merged cells, and can be split there.

A table without a header row

Markdown has no header-less table, so the editor writes one under a header row whose cells are all empty and adds data-headerless="true" to the table’s attribute line. Reading it back drops that row and the marker again, and the site renders the table with no <thead>. A header row whose cells are all empty and that carries no marker is a real header row left blank, and is kept. Every other row is kept too, including a body row whose cells are all empty, so a table no longer gains or loses rows on a save.

Size limits

A table is at most four columns wide for each cell it holds, and at most 1,000 columns — or as wide as its widest row has cells, when that is more. Each colspan is cut to the room left under that width when a table is pasted or dropped and when the page is saved, and a cut made on a save is reported. A table read from markdown is as wide as its delimiter row, so one wider than four columns a cell — a lone header cell spanning ten columns, say — is cut on its next save. No cell spans more than 1,000 columns or 65,534 rows, which are HTML’s own limits.

A page body holds at most 50,000 table cells in all, counting the empty cells added to fill a short row out to its header’s width. A write over that is refused with markdown.too_many_table_cells, and the message says which case it hit: more than 50,000 cells written, or more than 50,000 once each short row is filled out. Split the table across pages or publish the data as a downloadable file, or, in the second case, give the table fewer columns or fewer rows. The limit is judged on the whole body, so an anchored edit is refused when the body it would produce is over it, and the editor refuses an edit that would take the body over it. A body already over the limit can still be read, edited and saved, as long as an edit adds no cells.

Known limits

  • When Merge cells leaves no cell starting in a column, that column’s alignment goes with it: no cell is left to hold it, and the delimiter row writes the column unaligned. Split the cell and set the alignment again from the table menu.
  • The table menu reads the alignment only of selected columns where a cell starts; a column a span covers holds no alignment of its own.
  • A table saved before alignment and spans were kept may hold a cell width or background color that arrived with pasted HTML. Neither ever reached the published site, and neither is taken from a paste any more. The next save drops them without a report.
  • In the editor a cell’s class does not style the cell, so it cannot restyle the CMS itself. The published site renders it as a real class.

Everything that does change on a save is collected on Fidelity and Unsupported Syntax.

Wide tables on documentation pages

On a documentation or API page, a table wider than the article column scrolls sideways inside its own frame instead of squeezing its cells. The header row and the first column are held on one line, because those are the labels a reader scans; the other cells still wrap, so most tables never need to scroll at all. A short table still fills the column. The scrolling frame is added in the reader’s browser, so with JavaScript off a wide table is simply narrow, as it always was. There is nothing to write in the markdown for any of this.

Which page types allow tables

The table toggle is one of nine formatting switches on a page type, and it applies to posts-type page types only — blogs and similar. On a documentation or api page type the toggle does nothing, there is no control for it in Settings, and every formatting feature is available. If you are writing documentation and hunting for a switch to turn tables on, there is not one. What Each Page Type Lets You Format has the full picture, and inserting and editing a table in the CMS is Block Types and Block Settings.

ESC