HTML Table Generator
Lay out a table in a grid and get accessible markup — captions, header scopes and spans — entirely on this device.
Arrow keys move between cells; Home and End jump within a row; Enter or typing edits; Shift+Enter adds a line break inside a cell; Esc cancels an edit. Paste from a spreadsheet anywhere on this page to fill the grid.
Type in the grid or paste a table — the rendered table appears here.
Press ? for shortcuts
Do it offline:$ python3 -c "import csv,sys,html;print('<table>'+''.join('<tr>'+''.join(f'<td>{html.escape(c)}</td>' for c in r)+'</tr>' for r in csv.reader(sys.stdin,delimiter='\t'))+'</table>')" < table.tsv
How to use it
- Size the grid — add or drop rows and columns, then arrow between cells and type. A long value keeps its own cell; nothing reflows around it.
- Switch on a header row, a header column or a footer row. Each one decides which cells become th and which section of the table they land in.
- Merge where a label covers more than one cell, using the span buttons. The layout is validated first, so a slot claimed twice is named instead of emitted.
- Give the table a caption, choose a border preset and Pretty or Minified, then copy the markup, take the CSS beside it, or keep the layout as a link.
Writing a table by hand means counting cells and hoping the merges still line up. Lay it out in the grid instead: fill cells, merge across columns or down rows, mark the header strips, and the markup appears below with its sections in standard order and a scope on every header cell. The layout is checked before a single tag is written, and nothing you type leaves this tab.
Examples
Grid: Region / Q3 / Q4 — captioned, Basic borders, figures right-aligned<table class="data-table">
<caption>Quarterly revenue by region</caption>
<thead>
<tr>
<th scope="col">Region</th>
<th scope="col" class="align-right">Q3</th>
<th scope="col" class="align-right">Q4</th>
</tr>
</thead>
<tbody>
<tr>
<td>North</td>
<td class="align-right">1,204</td>
<td class="align-right">1,530</td>
</tr>
<tr>
<td>South</td>
<td class="align-right">998</td>
<td class="align-right">1,120</td>
</tr>
</tbody>
</table>Alignment travels as a class, never an inline style and never the obsolete align attribute, so the CSS tab carries a matching align-right rule. An empty class field mints data-table, because a rule needs a selector.
Service Owner Notes
api Ada "rotate keys
review quota"
web Noor static only<table>
<thead>
<tr>
<th scope="col">Service</th>
<th scope="col">Owner</th>
<th scope="col">Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>api</td>
<td>Ada</td>
<td>rotate keys<br>review quota</td>
</tr>
<tr>
<td>web</td>
<td>Noor</td>
<td>static only</td>
</tr>
</tbody>
</table>A copied range is split on tabs, and the one cell holding a break arrives quote-wrapped and stays whole. The two lines are then joined by a br this page writes: 1 multi-line cell emitted with <br> between the lines (row 2 column 3) — HTML renders a raw line break as a space.
Grid: room schedule — header column, a spanning header, merged cells, footer<table>
<caption>Weekend studio bookings</caption>
<thead>
<tr>
<th scope="col">Room</th>
<th scope="colgroup" colspan="2">Weekend</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Studio A</th>
<td>Rehearsal</td>
<td>Free</td>
</tr>
<tr>
<th scope="row">Studio B</th>
<td colspan="2">Closed</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">Total</th>
<td colspan="2">3 bookings</td>
</tr>
</tfoot>
</table>The header column turns each first cell into a row header, and a label merged across two columns takes scope="colgroup". That merge is also where scope runs out, which the note beneath the output says outright: 1 spanning header cell (row 1 column 2) — scope alone cannot say which cells a spanning header covers; that needs headers/id association, which this tool does not emit.
Grid: Field / Value — cells holding a script tag, an & and quote marks<table>
<thead>
<tr>
<th scope="col">Field</th>
<th scope="col">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>comment</td>
<td><script>alert(1)</script></td>
</tr>
<tr>
<td>query</td>
<td>a=1&b=2</td>
</tr>
<tr>
<td>label</td>
<td>He said "go"</td>
</tr>
</tbody>
</table>Seven characters became references here, counted on the Escaped chip. Feed the output to an HTML parser and the only elements it reports are table, thead, tbody, tr, th and td — the script tag comes back as cell text.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
rowspan="0" no longer means "to the end"
HTML 4 read zero as spanning the rest of the section. The current standard dropped that, so a zero is treated as one and a note says which cell it came from.
An empty cell and a missing cell differ
A blank value is a legal td and stays one. A slot no cell covers is a table-model error, so an empty td is inserted there and counted in a note.
Runs of spaces do not survive rendering
Two spaces, or a space at either end of a cell, collapse to one on screen. The characters stay in the markup, but a note warns that spacing is not layout.
Sections have a required order
caption first, then thead, tbody, and tfoot last. Older examples show tfoot ahead of tbody because HTML 4 demanded it; browsers accept both and the standard now specifies this one.
Common errors
row 2 column 2 is claimed twice — the cell at row 1 column 2 already spans across it; reduce that span or remove this cell.- Two cells cannot own one slot: the standard calls that a table model error, and browsers recover from it differently. The position given is the second claimant, so the merge to shrink is the one named.
colspan="1,001" at row 1 column 1 is outside 1–1,000 — the HTML standard clamps colspan to 1,000; use a value inside that range.- Those ceilings come from the parsing rules, not from this tool — 1,000 across, 65,534 down. A value beyond them would be silently clamped by the browser, which is a change to your table nobody asked for.
the table is 1 row × 51 columns — this tool's grid is limited to 1,000 rows × 50 columns- The refusal prints the measured size against both ceilings. Header and footer strips are not counted as data rows, and nothing is ever trimmed to fit — the command below handles a bigger file.
Class attribute contains "!" (U+0021) at position 9 — the class name is also the CSS selector this tool writes, so it may only hold letters, digits, hyphen and underscore — one name, no spaces; remove it or replace it with a hyphen.- The name you type is pasted into the CSS tab as a selector. Characters that mean something to a selector — a space is the descendant combinator — are refused with their position rather than escaped behind your back.
FAQ
Why does my table need scope?
Because a header cell on its own does not say what it heads. scope="col" states that it labels the column below, scope="row" that it labels the row beside it, and a screen reader replays that label while moving through the data. Merged headers take colgroup or rowgroup; past that, association needs headers and id, which this tool does not write.
When is a table the wrong element?
When the content is not a grid of related values. One column of items is a list; a page laid out in rows and boxes is a job for CSS. Both raise a warning here, because assistive technology announces a table as a table and then reads out coordinates that mean nothing.
Can I paste from Excel?
Yes. Copy a range and paste it in: cells arrive separated by tabs, and any cell holding a tab, a quote or a line break arrives wrapped in quotes and is read back whole. Short rows pad to the widest one and the count is reported. Merged source cells arrive flat — set the spans again here.
Keyboard shortcuts
- ⌘/Ctrl + C
- Copy the tab on show, markup or CSS, while nothing is selected.
- ⌘/Ctrl + Shift + C
- Copy that tab even when text is selected somewhere.
- ⌘/Ctrl + S
- Save the markup as an .html file.
- ? or ⌘/Ctrl + /
- Show every binding this page answers to.
- t
- Switch between the light and dark themes.
- Esc
- Close the topmost overlay.