Basic Tables
A table is the most predictable syntax in Markdown. There is no abbreviation to remember and nothing to nest: a table is a header row, a delimiter row, and any number of data rows, and the vertical bar does the splitting. What makes tables harder than they look is that the rules are enforced silently. A missing vertical bar does not produce an error message — the block quietly degrades into a paragraph of text that happens to contain a few bars. This page walks through the conditions a table has to meet, the rules that decide what happens when a row carries the wrong number of cells, and the two places where the vertical bar fights back: inline code and absolute-value notation.
Every HTML sample on this page was produced by running the source through a GitHub Flavored Markdown (GFM) parser, so what you see is what a renderer really emits rather than an illustration of it. If your own renderer disagrees with one of these samples, that is a useful fact about your renderer: the usual causes are a parser that predates the table extension, or a theme that restyles tables after the fact.
By the end of the page you should be able to write a table from scratch, look at a block of source and say whether it will render as a table, explain where the extra or missing cell in a short row goes, and escape a vertical bar so that it survives the trip through a cell.
A table is made of exactly three kinds of line. The first is the header row, which names the columns. The second is the delimiter row, which declares where the data begins. Everything after that is a data row:
| Column 1 | Column 2 | Column 3 |
| --- | --- | --- |
| A | B | C |
<table>
<thead>
<tr>
<th>Column 1</th>
<th>Column 2</th>
<th>Column 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td>B</td>
<td>C</td>
</tr>
</tbody>
</table>
The header row becomes the thead section, the data rows become the tbody section, and the delimiter row itself produces no output at all — it exists only in the source.
The delimiter row
The delimiter row is the line that turns a header into a table. Every cell in it contains nothing but hyphens, with an optional colon on the left, on the right, or on both sides to set the alignment of that column; the next page is entirely about those colons. How many hyphens you write is up to you: one is enough, three is the convention, and the count has nothing to do with the width of the column. Padding the cells so that the dashes line up with the header text is a habit worth keeping, because it makes the source readable, but a renderer does not require it.
What the renderer does require is that the delimiter row sit immediately under the header row, with the same number of cells. A blank line between them breaks the table apart, and both halves fall back to being ordinary paragraphs:
| a | b |
| --- | --- |
| 1 | 2 |
<p>| a | b |</p>
<p>| --- | --- |
| 1 | 2 |</p>
The vertical bars at the start and the end of a row are optional too. The GFM specification recommends them for readability, but the parser is happy without them, and you may mix the two styles from row to row. Keeping them on every row is still the better habit, because a missing outer bar is hard to spot in a long row while a missing inner bar is obvious.
The most common way a table fails is not a typo in the syntax but a row with the wrong number of cells. There are two separate rules here, and it pays to keep them apart.
The first rule governs the delimiter row: it must have the same number of cells as the header row, in either direction. If it does not, the table is not recognized at all and the whole block is shown as plain text:
| a | b |
| --- |
| 1 | 2 |
<p>| a | b |
| --- |
| 1 | 2 |</p>
The second rule governs the data rows, and here the header row is the reference. A data row with more cells than the header loses the excess, and a data row with fewer cells gets empty cells inserted until the widths match:
| a | b |
| --- | --- |
| 1 | 2 | 3 |
| a | b | c |
| --- | --- | --- |
| 1 | 2 |
Nothing is reported to you when this happens. The extra cell simply disappears, and the missing one becomes an empty cell that still occupies a column — which is why a table can look ragged without looking broken.
A cell holds inline content, so all the inline syntax you already know keeps working inside it: emphasis, inline code, links, images. A command written as pnpm install becomes a code element, and **all** becomes a strong element, exactly as they would in a paragraph.
| Command | Purpose |
| --- | --- |
| `pnpm install` | Installs **all** dependencies |
| `pnpm test` | Runs the [test suite](https://example.com) |
What does not work is block-level content. A cell cannot hold a paragraph, a list, or a code block: the specification says plainly that block-level elements cannot be inserted in a table. If a cell seems to need a list, the practical answer is to keep the cell short and put the detail in the text around the table.
Line breaks inside a cell
A cell is a single line of inline content, and you cannot break it with a newline — the newline ends the row and starts the next one. To force a line break inside a cell you have to ask for one explicitly, and the only portable way is an HTML break tag:
| Command | Description |
| --- | --- |
| `pnpm run dev` | Starts the <br> development server |
This one depends on the renderer allowing raw HTML, and plenty of them do not: a sanitizer that strips the break tag leaves the cell on a single line, which is harmless but quietly different from what the source asked for. If a line break has to survive everywhere, keep the cell to one line.
The vertical bar is the cell separator, so a literal vertical bar inside a cell has to be escaped with a backslash. The escape is applied while the row is being split into cells, which is why it works even inside inline code and even inside bold text:
| Expression | Note |
| --- | --- |
| `a \| b` | the escaped bar |
The cell comes out as a code element holding the bar itself, so the reader sees the bar as part of the expression rather than as a column separator. Leave the escape out and the row is split in two instead: the cell ends at the unescaped bar, the rest of the code span becomes the next cell, and the surplus cell is dropped because the header row only has two columns.
Formulas are where this bites hardest, because the absolute value of a quantity is written with a pair of vertical bars. Written raw, those two characters tear the cell apart. The safe spelling inside a table cell is the LaTeX pair \lvert and \rvert, which contains no vertical bar at all:
| Formula | Meaning |
| --- | --- |
| `\lvert A \rvert` | absolute value of A |
The escape is applied before the inline syntax is parsed, so a bar inside inline code must be escaped as well. The following source looks like it puts a piece of code in a cell, but the bar splits the row in two and the code span is cut in half:
| Written as | Note |
| --- | --- |
| `a | b` | the bar is not escaped |The row renders as two cells, not three: the first cell ends at the unescaped bar and the second cell holds the remainder of the code span. Write the bar as \| and the same cell comes out intact.
CommonMark, the specification that most parsers are built on, has no tables at all. Tables arrived as an extension in GitHub Flavored Markdown, and today most modern renderers support them — but the ones that do not are not broken, they are simply CommonMark. The same source that renders as a table on GitHub, on this site, or in most documentation tools comes out of a plain CommonMark parser as a single paragraph:
| Column 1 | Column 2 |
| --- | --- |
| A | B |
<p>| Column 1 | Column 2 |
| --- | --- |
| A | B |</p>
This is worth knowing before you paste a table into a tool that renders Markdown on its own, such as a wiki, an issue tracker with a plain Markdown mode, or a documentation generator with a strict dialect setting. The fix is rarely a different table syntax; it is a different renderer.
A table is a leaf block: it holds inline content, but it cannot contain other blocks. Two consequences follow for the blocks around it. The delimiter row must follow the header row immediately, and the table ends at the first blank line or at the start of another block-level structure — so a list, a heading, a code block, or a block quote written directly underneath closes the table rather than joining it.
A table that comes after a list needs a blank line in between. Without one, the table source is pulled into the list item as continuation text and no table is rendered at all:
- item one
| a | b |
| --- | --- |
| 1 | 2 |
The other direction deserves more care. A line of ordinary text placed directly under the last data row, with no blank line in between, is swallowed as another row of the table, and its missing cells are filled in as empty ones. Leave a blank line after the table and the text stays a paragraph.
| Written as | What actually happens | Why |
|---|---|---|
| A delimiter row with a different number of cells than the header | Not a table at all | The header and the delimiter row must have the same number of cells |
| A data row with one cell too many | The extra cell is dropped | The header row decides the column count |
| A data row with one cell too few | An empty cell is inserted | The header row decides the column count |
| An unescaped bar inside a cell | The row is split into more cells | The bar is the cell separator |
| An escaped bar inside a cell | The bar is shown as written | The escape is applied before the inline parsing |
| A blank line between the header and the delimiter row | Not a table at all | The delimiter row must follow the header row at once |
| A table written directly under a list item | The table joins the list item | A blank line has to separate the two blocks |
Rewrite the block below as a Markdown table with three columns: command, argument, and description. The commands and the arguments are inline code, the descriptions are bold, and the second argument contains a vertical bar that has to be displayed as written instead of splitting the row.
command pnpm install
argument --frozen-lockfile
description installs strictly from the lockfile
command pnpm publish
argument --access public | --tag latest
description publishes to the public registry
Approach:
A table needs a header row, a delimiter row with the same number of cells, and then the data rows. The cells of the delimiter row hold nothing but hyphens; the colons belong to column alignment and are not needed here. The delicate part is the second argument: the vertical bar in --access public | --tag latest has to be escaped as \|, otherwise it splits that row into three cells and the table collapses.
Steps:
- Write the header row
| command | argument | description |with three cells. - Write the delimiter row
| --- | --- | --- |right underneath it, with three cells and no blank line in between. - Write the first data row: the command as inline code
pnpm install, the argument as inline code--frozen-lockfile, and the description as**installs strictly from the lockfile**. - Write the second data row: the command as inline code
pnpm publish, the argument as inline code with the bar escaped,--access public \| --tag latest, and the description as**publishes to the public registry**. - Check the result: the table renders with three columns and two data rows, and the argument in the second row shows the vertical bar as written without splitting the row.
Answer:
| command | argument | description |
| --- | --- | --- |
| `pnpm install` | `--frozen-lockfile` | **installs strictly from the lockfile** |
| `pnpm publish` | `--access public \| --tag latest` | **publishes to the public registry** |You should now be able to write a table from scratch and say why a block of source will or will not render as one: the header row and the delimiter row must have the same number of cells, the delimiter row must follow the header row immediately, and every data row is padded or trimmed to the width of the header. You should be able to explain that a surplus cell in a data row is dropped and a missing one becomes an empty cell, that inline syntax works inside a cell while block-level content does not, and that a literal vertical bar has to be escaped — with \lvert and \rvert as the safe spelling for absolute values in a table. The next page takes the colons in the delimiter row one step further and shows how they control the alignment of each column.
