Building a Markdown Table From Scratch, Without Counting Dashes by Hand
Published 7/30/2026 · 13 min read · Developer tools
Daniel Okonkwo — Front-end developer and tech writer at Allin
Web performance · File formats
Checked against 4 sources
The minimum valid Markdown table is two lines, not three: a header row and a delimiter row under it. | Name | Role | followed by | ---- | ---- | is a complete, valid GitHub table with a header and no body — the spec says so explicitly, and the HTML it produces simply has no tbody. What you cannot do is leave out the delimiter row. A header line on its own is a paragraph with pipes in it, and it will render as literal text. That row is not decoration and not alignment sugar: in GitHub Flavored Markdown it is the signal that turns an ordinary paragraph into a table, which is why it is mandatory here while renderers with different table syntaxes do not need one. Two rules follow from it. The delimiter row must have exactly as many cells as the header, or the table is not recognised at all. And every other row may have any number of cells: too few and empty cells are inserted, too many and the excess is discarded. Leading and trailing pipes are optional — the spec recommends them for clarity — and the spaces that make columns line up have no effect on the output whatsoever. Pipe tables are not part of CommonMark. The core specification, version 0.31.2, defines leaf and container blocks and contains no table construct at all; GitHub's tables are an extension layered on top. So a table that renders perfectly in a pull request can render as raw text in a strict CommonMark processor, and the original 2004 Markdown had no tables either. Before you write one, check which renderer will read it.
The smallest thing that is still a table is two lines: a header row and a delimiter row. Here is why the second one is mandatory in GitHub Flavored Markdown, where pipe tables do not exist at all, and what a generator does that hand-typing cannot.
The smallest thing that is still a table
Type one line into the generator — Name,Role — and it produces two: | Name | Role | on top, | ---- | ---- | beneath. That is the whole table. It has a header, no body rows, and it is valid: the GitHub Flavored Markdown specification includes an example of exactly this shape and notes only that no tbody element is generated in the HTML. Nothing is missing. Whether that is useful is another question, but the syntax is complete.
Now delete the second line and you have nothing. A header row on its own is a paragraph that happens to contain vertical bars, and every GFM renderer will print it as one: Name | Role, as text, pipes and all. This is the single most common reason a hand-typed table fails, and the reason is structural rather than stylistic. Markdown parsers decide what a block is by looking at how it starts. A hash makes a heading, a greater-than sign makes a quote, four spaces make code. A paragraph containing pipes could be anything — a shell pipeline, a truth table, a bit of prose about probability. The delimiter row is the only thing that tells the parser this particular paragraph is a table, and there is no way to infer it without one.
The generator cannot forget it, and that is most of the value of using one. It builds the header, the delimiter row and every data row from a single column count, so the three can never disagree. Give it a header with no body and it emits the two-line table. Turn off the first-row-is-header switch and it invents Column 1, Column 2 and so on, because the syntax has no headerless form and inventing names is the only honest thing left to do. Hand it rows of unequal length and it squares them, filling the short ones with empty cells and telling you, above the output, how many rows it had to pad.
Where a pipe table simply does not exist
Markdown as John Gruber published it in 2004 has no tables. Read the original syntax document and you will find headers, blockquotes, lists, code blocks, horizontal rules, links, emphasis, images and inline HTML — and nothing about columns. Tables were never in it, and the escape hatch the document offers for anything it does not cover is to write raw HTML.
CommonMark, the effort to give Markdown an unambiguous specification, did not add them. Version 0.31.2, dated January 2024, defines leaf blocks — thematic breaks, headings, code blocks, HTML blocks, paragraphs — and container blocks — block quotes, list items, lists — and there is no table section. The specification itself acknowledges that some dialects extended the original syntax with conventions for footnotes and tables, which places tables firmly outside the core. GitHub's tables live in a separate document, the GFM spec, under the heading Tables (extension).
The practical consequence is that where your table renders depends on which extensions the processor has switched on, not on Markdown as such. A repository README on GitHub is safe. A static-site generator, a documentation build, a chat client, a comment box in a bug tracker: each one is a separate decision by whoever assembled it. If the destination matters, the two-minute test is to paste the two-line minimum table there and look. If it renders as a table with one header row, the extension is on and everything else will work; if it renders as a line of text with dashes under it, write the HTML instead, or use a list.
What the generator counts that you cannot
The delimiter row has a floor of three characters, and that is not arbitrary: :-: is the shortest cell that can still carry an alignment marker at each end. The generator applies that floor to every column, so a one-letter header comes out as | a | over | --- |, never | a | over | - |. It also computes each column's width from the widest cell in the column, header included, which is the arithmetic nobody wants to do by hand on a table of twelve rows.
The interesting part is how it measures a cell, because a character is not a column. This generator counts display columns, not characters: the ideographs 東京 are two characters and it counts four, an é written as e plus a combining accent is two characters and it counts one, a single emoji is two UTF-16 units and it counts two. Counting characters is what a naive generator does, and it skews the source the moment the data leaves the Latin alphabet. A family emoji is the case that catches naive width code out: four people welded together by zero-width joiners, seven code points, eleven UTF-16 units, and one glyph two columns wide. This generator reports two, because a zero-width joiner takes no column of its own and costs the code point after it its own as well. None of this reaches the rendered table — the padding exists so the source lines up in an editor, and that is the only place it can be right or wrong.
There is a compact mode that turns the padding off entirely. With it, the separator keeps its three characters — | :-- | :-: | --: | for left, centre and right — and every other cell is written with no padding at all. It is the setting you want for a wide table in a versioned file, because a padded table re-flows its whole column whenever one value gets longer, and a one-word edit becomes a diff that touches every row.
Editing a table you already have
The generator will take an existing Markdown table as input, which is the fastest way to add a column or fix a typo without re-aligning anything. Paste the table, the delimiter is detected as the pipe, the leading and trailing empty cells are stripped, the row of dashes is recognised and dropped, and what remains is your grid. Escaped pipes survive the trip: a cell reading ps \| grep is unescaped back to ps | grep on the way in and re-escaped on the way out, so the table comes back identical rather than one column wider. The CSV converter on this site closes the same loop from its own end: it escapes the backslash before the pipe, so a cell that already held \| survives being converted again.
The alignment survives the trip as well, which is less obvious than it sounds, because the row that carries it is the row that has to be thrown away. The tool reads the colons off the delimiter row before filtering it out — :--- is left, ---: is right, :---: is centred, a plain --- says nothing — and seeds each column's setting from what it read. Paste a table whose rule reads | :--- | ---: | and the right-aligned number column comes back right-aligned. What you set by hand still wins: the per-column selectors and the Default, Left, Center and Right chips above the table override whatever was read, so you can change an alignment on purpose. What you cannot do is lose one by accident.
| Part | Required? | What happens if it is missing or wrong |
|---|---|---|
| Header row | Yes | There is no headerless form; the generator invents Column 1, Column 2 rather than omit it |
| Delimiter row of dashes | Yes | No table at all — the header renders as a paragraph with pipes in it |
| Delimiter cell count matching the header | Yes | The table is not recognised and falls back to literal text |
| Body rows | No | A header plus a delimiter row is a valid table; the HTML simply has no tbody |
| Cell count in a body row | No | Too few and empty cells are inserted; too many and the excess is ignored |
| Leading and trailing pipes | No | Optional; the spec recommends them for clarity and to avoid parsing ambiguity |
| Padding spaces | No | No effect on the output; they exist so a human can read the source |
| A blank line inside the table | Never | The table is broken at the first empty line or the start of another block |
Frequently asked questions
- What is the smallest valid Markdown table?
- Two lines: a header row and a delimiter row beneath it. | Name | over | --- | is a complete table with one column, one header cell and no body, and the GFM specification includes an example of exactly that, noting only that no tbody element appears in the HTML. You cannot go smaller. There is no one-line table, and there is no way to have body rows without a header — if your data has no natural header, the generator writes Column 1, Column 2 and so on, which is what the syntax forces. The smallest cell in the delimiter row is three characters, because :-: is the shortest form that still carries an alignment marker at both ends.
- Why does GitHub need the row of dashes when some other renderers do not?
- Because a pipe means nothing in Markdown. Every other block construct announces itself with a character at the start of the line — a hash, a greater-than sign, a dash, a number and a dot — but a line containing pipes is indistinguishable from prose. GitHub's table syntax solves that by requiring a second line whose cells contain only hyphens and optional colons, which no paragraph would ever produce by accident. Renderers with a different table syntax do not have the problem because they mark tables differently: some wikis use a distinct opening token, and formats such as reStructuredText draw the table with a grid of characters. The requirement is a consequence of the notation, not a rule GitHub invented to be strict.
- Can I have a table with no header?
- Not in the syntax. The first row is always the header, and the delimiter row always sits under it, so a headerless table cannot be expressed. The generator handles this by inventing names — Column 1, Column 2 — when you untick the first-row-is-header switch, which is a compromise rather than a solution. Two alternatives if the header genuinely has no content: give the columns empty cells, which is legal and renders as an empty header row on GitHub, or write the table as HTML and skip the thead entirely. The empty-header trick usually looks better than the invented names, and it is one edit away from either.
- My table renders on GitHub but not in my documentation site. Why?
- Because tables are an extension and your documentation build did not enable it. CommonMark 0.31.2 has no table construct at all, so any processor that implements the core specification and nothing else will treat your table as three ordinary paragraphs. GitHub's own tables live in a separate document under the heading Tables (extension). Most static-site generators do support them, but support is a plugin or a configuration flag rather than a given. Check the Markdown configuration of the build, look for a GFM or pipe-table option, and if there is none, either add the plugin or fall back to an HTML table, which every renderer that permits inline HTML will show correctly.
- If I paste a table back in to edit it, does it keep its alignment?
- Yes. The delimiter row still has to be removed — otherwise the dashes would arrive as a data row — but the colons are read off it first and become each column's starting alignment, so :--- comes back as :---, ---: as ---:, :---: as :---: and a plain --- stays plain. All four GFM forms round-trip. The rest of the trip is faithful as well: leading and trailing empty cells are stripped, and an escaped \| is unescaped on the way in and re-escaped on the way out, so a cell reading ps \| grep does not turn into two columns. Anything you choose yourself still overrides what was read — pick an alignment in a column's selector, or one of the chips above the table, and yours wins. That is the intended order: the pasted table proposes, your setting disposes.
Articles you may find interesting
All guides →Related tools
This describes how a file format and a renderer behave, verified against the specification cited and against the tool's own code as it stands today. Renderers disagree: GitHub, GitLab, a static-site generator and your editor's preview are four different implementations, and a construct that works in one may not work in another. Nothing here is a guarantee about your pipeline — test the output where it will actually be published, and treat any tool, this one included, as something to check rather than something to trust.
Sources
- GitHub — GitHub Flavored Markdown Spec, version 0.29-gfm (2019-04-06), section 4.10 Tables (extension): the delimiter row is required and must match the header row in cell count; body rows may vary in length, with missing cells inserted empty and excess cells ignored; leading and trailing pipes are recommended but optional; the table is broken at the first empty line or the beginning of another block-level structure
- CommonMark — CommonMark Spec version 0.31.2 (2024-01-28): the core specification defines leaf blocks (thematic breaks, headings, code blocks, HTML blocks, paragraphs) and container blocks (block quotes, list items, lists) and contains no table construct — tables are named in the introduction as one of the extensions others added to the original syntax
- Daring Fireball — Markdown: Syntax, the original 2004 specification by John Gruber — covers headers, blockquotes, lists, code blocks, horizontal rules, links, emphasis, images and inline HTML, and contains no table syntax at all; the documented fallback for anything it does not cover is to write HTML
- GitHub Docs — Organizing information with tables: the practical rules as GitHub documents them, including escaping a pipe as \| inside a cell and the fact that the vertical bars of a row do not need to line up
Spotted a mistake in this article?