Tables
A Wit table is one node. Hand it rows and it draws a full <table> — caption, header, body — with no grid of pipes to align.
Rows can come as inline text, as a collection of records, or as / child nodes when cells need links and emphasis. tableis dispatched to a dedicated renderer, so it does not follow the generic id/class
allowlist — the parameters below are the whole surface.
Which form?
| You have | Use | Header comes from |
|---|---|---|
| rows typed out as text | inline CSV | row 0 by default |
| a collection + column order | schema-as-array | the schema keys |
| a collection + custom labels | schema-as-record | the schema labels |
| a collection and want auto columns | rows-by-ref | the first record's keys |
| cells with links or emphasis | body form (row/col nodes) | first row unless header false |
Inline rows
Pass a list of rows in the rows parameter. Row 0 is the header; every
later row is a body row. A caption rides above the table.
@table |rows [[Format, Extension], [Web page, .html], [Markdown, .md], [Print, .pdf]]| |caption Output formats|
| Format | Extension |
|---|---|
| Web page | .html |
| Markdown | .md |
Emits:
<table><caption>Output formats</caption><thead><tr><th>Format</th><th>Extension</th></tr></thead><tbody><tr><td>Web page</td><td>.html</td></tr><tr><td>Markdown</td><td>.md</td></tr><tr><td>Print</td><td>.pdf</td></tr></tbody></table>
Another, with the caption pipe closed before table@:
@table |rows [[Client, Hours], [One to One, 62.75], [Couple, 0.75]]| |caption Practicum| table@ → <table><caption>Practicum</caption><thead><tr><th>Client</th><th>Hours</th></tr></thead><tbody><tr><td>One to One</td><td>62.75</td></tr><tr><td>Couple</td><td>0.75</td></tr></tbody></table>Fixture:
19-tables/inline-csv.wit.
Drop the header with |header false| — the first row becomes a
plain body row:
@table |rows [[Web page, .html], [Markdown, .md]]| |header false| → <table><tbody><tr><td>Web page</td><td>.html</td></tr><tr><td>Markdown</td><td>.md</td></tr></tbody></table>Fixture:
19-tables/no-header.wit.
From a data collection
Point rows at a collection of records and each record becomes a row.
With no schema, columns are auto-derived from the first record's keys.
The renderer prefers the expander's typed collection over literal text.
#packages: [ { name - parser, role - reads source }, { name - runtime, role - resolves data } ] @table |rows @packages|
| name | role |
|---|---|
| parser | reads source |
| runtime | resolves data |
Emits headings name / role over the two records. A second
example, with the full emit:
#sales: [ { region - West, total - 100 }, { region - East, total - 90 } ] @table |rows @sales| table@ → <table><thead><tr><th>region</th><th>total</th></tr></thead><tbody><tr><td>West</td><td>100</td></tr><tr><td>East</td><td>90</td></tr></tbody></table>
To choose and label the columns yourself, add a schema.
Schema-as-array (positional)
Schema keys pick record fields in order and become the headings; every row is data (no header consumed).
#sites: [{ name - Dunmore Head, status - operational }, { name - Carrick Point, status - maintenance }] !! @table |schema [name, status]| |rows @sites|
| name | status |
|---|---|
| Dunmore Head | operational |
| Carrick Point | maintenance |
Emits headings name / status over the two records. Fixture: 19-tables/schema-array.wit.
Schema-as-record (labelled)
The record key selects the field; the record valueis the column heading.
@table |schema { name - Site, status - Status }| |rows [[Dunmore Head, operational], [Carrick Point, maintenance]]|
| Site | Status |
|---|---|
| Dunmore Head | operational |
| Carrick Point | maintenance |
Emits headings Site / Status (not name / status). The same works pointing at a record collection — |schema { name - Package, role - Role }| over @packages. Fixture: 19-tables/schema-record.wit.
Loading the records from a file at build time works the same way; see Data.
Rich cells: the body form
The only form whose cells hold rich content — links, emphasis,
nested nodes — rather than plain strings. Leave off the rows parameter
and the renderer reads the body: a @row per row, a @col per cell.
@table |caption Modules| @row @col Module col@ @col Notes col@ row@ @row @col Parser col@ @col Reads *source*; see @a |href /docs/reference/syntax/| the syntax a@. col@ row@ table@
| Module | Notes |
|---|---|
| Parser | Reads source; see the syntax. |
Emits:
<table><caption>Modules</caption><thead><tr><th>Module</th><th>Notes</th></tr></thead><tbody><tr><td>Parser</td><td>Reads <strong>source</strong>; see <a href="/docs/reference/syntax/">the syntax</a>.</td></tr></tbody></table>
The first @row is the header — its cells render as <th> — unless you pass header false. A @th cell is always a header cell, even inside a body row. The
HTML-style names @tr / @td / @th work as aliases for @row / @col, and the whole row
can sit on one line: @row @col a col@ @col b col@ row@.
@row and @col are interchangeable:
the outer node is the row, the inner one the cell, whichever keyword you use. So @col @row a row@ @row b row@ col@ builds the same row as @row @col a col@ @col b col@ row@.
Data-driven rows with rich cells
This is the form the others can't reach: rows generated from data, with cells that
still hold nodes. Wrap the @row line in (each … as …) and pull fields into each cell body.
#modules: [ { name - Parser, status - stable }, { name - Runtime, status - stable } ] @table |caption Modules| @row @col Module col@ @col Status col@ row@ (each @modules as m) @row @col @m.name col@ @col @em @m.status em@ col@ row@ (end) table@
| Module | Status |
|---|---|
| Parser | stable |
| Runtime | stable |
Emits:
<table><caption>Modules</caption><thead><tr><th>Module</th><th>Status</th></tr></thead><tbody><tr><td>Parser</td><td><em>stable</em></td></tr><tr><td>Runtime</td><td><em>stable</em></td></tr></tbody></table>
The header row stays fixed; the loop appends one body row per record. Put an (if …) inside the loop to filter which rows appear — here only the
stable modules:
#modules: [ { name - Parser, status - stable }, { name - Sandbox, status - draft } ] @table |caption Stable modules| @row @col Module col@ @col Status col@ row@ (each @modules as m) (if @m.status is stable) @row @col @m.name col@ @col @em @m.status em@ col@ row@ (end) (end) table@
| Module | Status |
|---|---|
| Parser | stable |
The draft row drops out; only Parser survives:
<table><caption>Stable modules</caption><thead><tr><th>Module</th><th>Status</th></tr></thead><tbody><tr><td>Parser</td><td><em>stable</em></td></tr></tbody></table>
One limitation: data resolves in the body, not in a parameter
A field reference resolves inside a cell body — @col @m.name col@ works — but not inside a parameter. A link's hrefcannot yet be @m.url:
@col @a |href @m.url| @m.name a@ col@ ✗ the href is not resolved @col @a |href /modules/parser| @m.name a@ col@ ✓ literal href, data in the text
Keep every data-driven value in the cell body and write parameters as literals.
Header and caption controls
| Param | Effect |
|---|---|
| caption text | prepends a caption element |
| header false | no header row |
| header N | use row index N as the header |
| header [a b c | |
| explicit header cells — all rows become data |
Edge cases and value rules
| Case | Behaviour | Fixture |
|---|---|---|
| empty or missing rows | empty table element | 19 empty |
| multi-line cell content | preserved | 19 multiline-cell |
| number or boolean cell | String(value) | 19 tables |
| null cell | empty cell | 19 tables |
| comma inside a cell | splits the cell — avoid | verified |
| quoting a cell | not supported inline — quotes stay literal | verified |
@table table@ → <table></table>
Centering is theme, not renderer
The default HTML pathway centres tables via .wit-doc table { border-collapse: collapse; margin: 1em auto } in
the theme. --raw ships a reset-only base, so it does not centre —
add your own CSS. Fixture: 19-tables/* and the theme.
See also
- Logic & assembly —
(each …)and(if …)in full, the engine behind data-driven rows. - Data — defining and loading the record collections a table reads.
- Syntax reference — every table parameter and the exact HTML it emits.
- Data model · Core vocabulary.