Skip to content
Allin

Building a Markdown Table From Scratch, Without Counting Dashes by Hand

Published 7/30/2026 · 13 min read · Developer tools

Daniel Okonkwo

Daniel OkonkwoFront-end developer and tech writer at Allin

Web performance · File formats

Checked against 4 sources

View profile
In short

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.

Every part of a GFM table: what it is, whether it is required, and what happens when it is wrong
PartRequired?What happens if it is missing or wrong
Header rowYesThere is no headerless form; the generator invents Column 1, Column 2 rather than omit it
Delimiter row of dashesYesNo table at all — the header renders as a paragraph with pipes in it
Delimiter cell count matching the headerYesThe table is not recognised and falls back to literal text
Body rowsNoA header plus a delimiter row is a valid table; the HTML simply has no tbody
Cell count in a body rowNoToo few and empty cells are inserted; too many and the excess is ignored
Leading and trailing pipesNoOptional; the spec recommends them for clarity and to avoid parsing ambiguity
Padding spacesNoNo effect on the output; they exist so a human can read the source
A blank line inside the tableNeverThe table is broken at the first empty line or the start of another block
Markdown Table GeneratorPaste CSV, TSV or semicolon data and get an aligned GitHub-flavoured Markdown table, with per-column alignment, pipe escaping and a live preview.Try the tool

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
GuidePasting a Table Into a Pull Request: What Breaks, and the Two Characters That Break ItA Markdown table has exactly two forbidden characters in a cell: the pipe and the line break. Here is what each one does, how a converter handles them, why the escape has to be applied in the right order, and why padding never matters.How-toMarkdown: A Beginner's GuideFormat plain text with a few symbols: # for headings, ** for bold, - for lists. Here's what markdown is, the core syntax, why it's everywhere, and the gotchas.GuideTransposing a Table Whose Rows Should Have Been ColumnsWhat happens to the header row, what happens to rows of unequal length, what happens to types — and the one thing transposing is regularly mistaken for and cannot do.GuideMarkdown Task Lists and What Actually Renders WhereTask lists are not in CommonMark. They are a GitHub Flavored Markdown extension, which is why the same file shows checkboxes in one place and literal brackets in another. The exact marker rule, what nesting does, and a table of what is CommonMark, what is GFM and what is neither — checked against both specs and four renderers.ExplainerWord Count: Reading Time and LimitsA word count is words separated by spaces. Here's how it's counted, why limits exist, how it maps to pages and reading time, and when characters count instead.GuideBeautify or Minify: What Each Is For, and What It Does to the WeightFour real stylesheets run through the minifier, measured raw and after gzip. Stripping every space saved 48, 103, 104 and 147 compressed bytes; stripping comments saved 57, 1 358, 2 420 and 1 042. Plus the five inputs this minifier breaks.

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

Spotted a mistake in this article?