The table that looks fine until it renders
Markdown tables are deceptively simple: a row of cells separated by pipes, a dashed row underneath to mark the header, more rows below. Almost everyone who writes one by hand eventually hits the same handful of failure modes, because the syntax has a few sharp edges that don’t show up until a renderer disagrees with what you meant.
A pipe inside the table breaks the table
The pipe character separates cells. So what happens when a cell’s actual content needs a literal
pipe — a shell command with cmd1 | cmd2, a regex alternation, a column that legitimately means
“or”? Written unescaped, it splits the row into an extra cell and misaligns everything after it.
There are two ways around this, and most people only know one:
- Escape it:
\|renders as a literal pipe without splitting the cell. This works everywhere. - Put it in a code span:
`cmd1 | cmd2`— inside backticks, a pipe doesn’t need escaping in most renderers, because the code span is parsed as a unit first. This is the more common source of breakage, because a lot of naive table parsers (including some tools that claim to handle Markdown tables) split on every pipe character without checking whether it’s inside a code span first, and quietly produce a wrong table with no error.
A ragged row isn’t invalid, but it’s ambiguous
If a data row has fewer cells than the header, most renderers pad the row with empty cells rather than rejecting it — which is convenient when you’re editing by hand and forget a trailing pipe, but means a genuine mistake (a value that got deleted) looks identical to an intentional empty cell. There’s no syntax-level way to tell them apart. If a table has an unusual number of empty cells, that is generally a sign the row was edited unsafely, not that the data really is missing.
The alignment row is not decorative
The row of dashes under the header (| --- | --- |) isn’t just formatting — it’s what makes the
table a table in the first place, in the strict version of the Markdown table syntax (GFM). Leave it
out and some renderers won’t recognize the block as a table at all; they’ll print it as a literal
paragraph of pipe characters. The colons around the dashes control alignment: :--- for left,
:---: for center, ---: for right, plain --- for the default. Nothing about the dash count
matters beyond “at least one” — --- and ---------- mean the same thing to a renderer, which is
why hand-padding a table to make it look aligned in the source is purely cosmetic. It helps a human
reading the raw file and makes zero difference to the rendered output.
CJK text and emoji break naive alignment
If you do pad a table by hand — or write a script that does — the natural approach is to count characters and pad with spaces to match. That works for plain Latin text and silently produces a ragged-looking table the moment a cell contains CJK characters or most emoji, because those render about twice as wide as a Latin letter on screen despite counting as a single character in code. A table padded by character count looks aligned in a monospace editor and looks aligned in the source file, but visibly isn’t once you look at columns mixing English and, say, Japanese names side by side. Getting this right requires measuring display width — the East Asian Width property, plus the emoji ranges — separately from character count.
Where the actual data usually lives
Most people don’t type a Markdown table by hand at all — they paste it from somewhere. A spreadsheet
paste arrives as tab-separated text with no pipes anywhere. A CSV export has its own escaping rules
(quoted fields, doubled quotes for a literal quote) that have nothing to do with Markdown’s. An HTML
table copied from a webpage carries <td>/<th> structure that has to be read correctly, including
knowing what to do when a cell spans multiple columns — something Markdown tables can’t represent at
all. Getting a clean Markdown table out the other end usually means solving the other format’s
parsing problems first, then applying everything above on the way out. This is the same reason a
block of text pasted straight out of a PDF needs its own cleanup pass before it reads as normal
prose again — see
cleaning up text pasted from PDFs for the paragraph
version of the same problem.
A CSV field is not a Markdown cell
The escaping rules genuinely don’t transfer between formats, which is worth spelling out because it
trips up hand-written converters more than anything else on this list. A CSV field that contains a
comma has to be wrapped in quotes; a quote inside that field has to be doubled. None of that has
anything to do with the pipe character, which is CSV-legal and needs no escaping at all inside a
quoted CSV field. Feed a CSV export through a converter that only knows how to escape pipes, and a
value like "Smith, Jane" comes out fine while a value containing an actual pipe character slips
through unescaped and quietly breaks the Markdown table on the other side. The two formats’ unsafe
characters barely overlap, so a converter has to know both rule sets, not just one applied twice.
Header rows do double duty
A Markdown table’s first row is always the header, by construction — there is no way to write a
Markdown table without one. CSV, TSV, and a plain HTML <table> without a <thead> don’t have that
guarantee; the first row might be a header, or it might just be the first record. A converter that
assumes the first row is always a header will occasionally turn real data into column labels, and
one that never assumes it will occasionally do the opposite. The only reliable fix is exposing the
choice — let whoever is converting the table say whether that first row is a header, rather than
guessing silently in either direction and hoping it’s right.
Building instead of hand-editing
The Markdown table formatter handles the pipe-in-code-span case, pads ragged rows instead of silently dropping data, measures alignment by display width so CJK and emoji line up correctly, and converts between Markdown, CSV, TSV, HTML, JSON, and reStructuredText grid tables — including reading a straight spreadsheet paste, which arrives as tab-separated text, without any extra step. Paste in whatever shape the data is actually in and get a table that renders the way you meant it to, on the first try.