Pasting a Table Into a Pull Request: What Breaks, and the Two Characters That Break It
Published 7/29/2026 · 15 min read · Developer tools
Daniel Okonkwo — Front-end developer and tech writer at OneKitly
Web performance · File formats
Checked against 4 sources
A Markdown table cell can hold anything except two characters. A pipe closes the cell, so it has to be written as \| — the converter does this for you: paste USB-C | 2 m and it emits USB-C \| 2 m, which GitHub renders as one cell. A line break closes the row, and there is no escape for it at all: a cell containing a real newline is impossible in GFM table syntax, so the tool replaces it with the HTML tag <br>, which is the only thing that works. Under the header goes the delimiter row, and it is mandatory — a header line with no row of dashes beneath it is not a table and GitHub renders it as a paragraph with pipes in it. That row also carries the alignment: --- leaves the renderer's default, :-- is left, --: is right, :-: is centred, and a colon anywhere else does nothing. The delimiter row must have the same number of cells as the header or the table is not recognised at all. The spaces that make the source line up are cosmetic and nothing more: | a | b | and |a|b| produce byte-identical HTML, which is why the converter has a Pad columns switch and why turning it off is safe. One detail about the escaping, because correct output looks wrong here: the backslash is escaped before the pipe. A cell that already reads x \| y comes out as x \\\| y, where \\ is a literal backslash and \| is a literal pipe, so GitHub renders the single cell x \| y. Escaping the pipe alone would give x \\| y — a literal backslash followed by a live cell break — and the column would split. Order is the whole of it, and it is why an existing Markdown table can go back through the CSV converter and come out unchanged.
A 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.
Two characters, and only two
Almost everything survives a Markdown table cell untouched. Accents, ideographs, emoji, backticks, asterisks, brackets, dollar signs, quotes — none of them mean anything to the table parser, which only ever looks for two things. The pipe ends a cell. The line break ends a row. That is the whole grammar, and every problem you will ever have pasting a table into a pull request is one of those two characters arriving where the parser did not expect it.
The pipe has an escape. Write \| and the parser reads a literal vertical bar instead of a column boundary. The converter applies it for you: its own sample data contains the cell USB-C | 2 m, and running the sample through it yields the row | Câble | USB-C \| 2 m | 9.90 |, which is one row of three cells and not one row of four. Feed it a cell that is nothing but a pipe and you get \| on its own, still one cell. This part is solid, and it is the part most hand-written tables get wrong: a shell command in a documentation table — ps | grep node — silently gains a column and pushes every value one place to the right, which is the kind of defect a reviewer reads past because the table still looks like a table.
The line break has no escape, and this is the part worth internalising: a cell containing a newline is not merely awkward in GFM table syntax, it is impossible. The row ends at the newline, full stop. There are exactly two honest ways out. Replace the break with the HTML tag <br>, which GitHub permits inside a cell and which is what the converter does — a quoted CSV field reading "line one\nline two" comes out as line one<br>line two in a single cell. Or accept that the content does not belong in a table at all and put it in a list or a paragraph underneath. Anything else is wishful thinking; there is no backslash trick, no doubled pipe, no continuation marker.
The delimiter row is not decoration
GitHub Flavored Markdown calls the row of dashes under the header the delimiter row, and the specification is unambiguous about it: it consists of cells whose only content is hyphens, with an optional leading or trailing colon, or both. Without it there is no table. A header line and three data lines with no dashes between them render as four paragraphs of text with pipes in them — which is why a table that looked fine in one editor and broke in the pull request almost always lost that one line to a copy-paste.
Four shapes of delimiter cell mean four different things, and the converter offers all four as an Align setting. A plain --- leaves the alignment to the renderer, which in practice means left in every engine anyone uses. A leading colon, :---, forces left. A trailing colon, ---:, forces right, and is the one you want on any column of numbers. A colon at both ends, :---:, centres. A colon anywhere in the middle of the dashes is not alignment and not valid — the cell has to be colon, dashes, colon, in that order, with nothing else in it. Set the tool to right and a two-column table comes out with | -----: | ---: | underneath, and every number in the column lines up on its last digit in the rendered output.
One rule catches most silent failures: the delimiter row must have the same number of cells as the header row, or the table is not recognised at all. The specification states it outright, and the failure mode is the worst kind — you do not get a broken table, you get no table. A header of four columns over a delimiter row of three renders as literal text, pipes and all. This is exactly what happens when someone adds a column to a table by hand and forgets the dashes. The converter cannot make that mistake because it builds both rows from the same column count, which is a decent argument for generating the table rather than editing one.
Padding is for you, not for GitHub
The converter has a Pad columns switch, on by default, and it changes nothing about the rendered table. With it on, a two-column table comes out as | Item | Qty |, | ------ | --- |, | Widget | 3 |, with the cells squared up. With it off you get | Item | Qty |, | --- | --- |, | Widget | 3 |, ragged. GitHub produces identical HTML from both. The spaces exist so a human reading the diff can see the columns, and for no other reason. Turn padding off when the table is wide and the diff is what people will read; leave it on when the file is edited by hand.
There is a catch worth knowing about if your data is not Latin. The padding is computed by counting characters, and a character is not a column. Feed the converter a cell containing two emoji and it counts two, pads to two, and the source no longer lines up in an editor where each emoji occupies two monospace columns. The same happens with Chinese, Japanese and Korean text, in the other direction from an é written as e plus a combining accent, which is two characters in one column. None of it affects the rendered table, so it is a readability problem and not a correctness one — but if you are producing a wide table of Japanese place names, do not expect the source to look tidy.
Escaping the escape, and guessing the delimiter
A converter that turns every pipe into \| and stops there is wrong on exactly one input, and it is one you meet the moment you re-import a table you wrote yourself: a cell that already contains \|. Escape the pipe alone and x \| y becomes x \\| y, where the doubled backslash is a literal backslash and the pipe behind it is live again — the column splits and nothing warns. The order has to be the other way round. This converter escapes the backslash first and the pipe second, so x \| y comes out x \\\| y, which looks like one backslash too many and is not: GFM reads \\ as a literal backslash and \| as a literal pipe, and renders the single cell x \| y.
The consequence is that a round trip is safe. Convert some data to a Markdown table, copy the table out, save it as CSV, feed it back in — the columns that held pipes come back as they went in, along with everything else. That is worth more than it sounds, because exporting a rendered table to a spreadsheet and re-importing it is an ordinary thing to do the moment a column gets renamed. The sibling tool on this site, the Markdown table generator, closes the same loop from the other end: its parser reads \| as a literal pipe when the delimiter is a pipe and unescapes it on the way in, so a table pasted there comes back identical too.
The delimiter is guessed rather than declared, and the guess is made across records rather than on the header alone. The tool counts commas, semicolons, tabs and pipes outside quotes on up to five records, and a delimiter that gives the same count on every record read beats one that is merely more numerous on the first. That is what settles a header like A|B|C,D: on its own it looks like three pipe-separated columns, but put comma-shaped rows under it and the comma wins, because the comma is the count that repeats. A French export whose header is Nom;Prénom is detected as semicolon-separated either way. The one thing the first record still decides on its own is which delimiters are candidates at all — a header containing none of the four leaves nothing to choose between, and the guess falls back to a comma. If your first line is unusual, set the delimiter by hand.
A workflow that survives review
Export the data as CSV rather than copying cells out of a spreadsheet, because the quoting rules of a CSV file are the only thing that tells the converter where a field with a comma or a line break ends. Paste it, check the delimiter it detected, choose your alignment — right for numbers, default for everything else — and read the first two output lines before you copy. Those two lines are the header and the delimiter row, and if they have a different number of pipes, nothing downstream will render.
Then look for the three cells that cause trouble. Anything with a pipe: check it came out as \| and not as a live column boundary, and remember that a cell which already held a backslash gets that backslash doubled — \\\| is the correct output, not a stray escape. Anything that was multi-line: check it became <br> and decide whether that is really what you want in a table. Anything empty: an empty cell is perfectly legal and renders as an empty cell, so a row of blanks in the middle of your table is data, not damage. If the table is going into a repository rather than a comment, commit it once with padding on so the first reviewer can read the diff, and never reformat it again — a whitespace-only change to a table is thirty lines of noise in a pull request that says nothing.
| Cell content | What the tool emits | Result |
|---|---|---|
| USB-C | 2 m | USB-C \| 2 m | Correct — one cell containing a visible pipe |
| A quoted field holding a real newline | line one<br>line two | Correct — the only thing GFM tables allow; a real newline is impossible |
| x \| y (already escaped) | x \\\| y | Correct — the backslash is escaped first, so GitHub renders the one cell x \| y |
| An empty field | Nothing between the pipes | Correct — an empty cell is legal and renders empty |
| A row with fewer fields than the header | Padded with empty cells to the widest row | Correct — a ragged table would not render at all, so the tool squares it |
| Two emoji | Padded as if they were two columns wide | Cosmetic only — they occupy four; the rendered table is unaffected |
Frequently asked questions
- Can I put a line break inside a Markdown table cell?
- Not a real one. The newline is what ends a row, so the table syntax has no way to express a cell containing one — there is no escape sequence for it the way \| exists for the pipe. The workaround GitHub accepts is the HTML tag <br>, which is what this converter substitutes: a quoted CSV field with a line break in it becomes one cell reading line one<br>line two. Two honest limits on that. Renderers that strip HTML will show the tag as literal text, and a cell with three or four <br> in it is usually a sign the content wants to be a list under the table rather than a cell inside it.
- My table renders as plain text with pipes in it. What did I break?
- Almost always the delimiter row. Either it is missing entirely, or it has a different number of cells from the header — the specification says the header row must match the delimiter row in cell count, and if it does not, the table is not recognised and falls back to a paragraph. Count the pipes on line one and line two of your table; they should be the same. The other common cause is a blank line between the header and the delimiter row, which ends the table block before it starts. A third, rarer one: the delimiter cells must contain nothing but hyphens and optional edge colons, so a stray space-dash-space or an em dash pasted by an editor's autocorrect will invalidate the row.
- Do the spaces that line up the columns matter?
- No. | a | b | and |a|b| produce the same HTML on GitHub, and the converter's Pad columns switch exists purely so the source is readable in an editor. Padding does have one real cost in a repository: because the width of every column is the width of its longest value, editing one cell can change the padding of the whole column, and a one-word change turns into a diff touching every row. If the table lives in a versioned file and is edited often, generating it unpadded produces cleaner diffs. If it is written once and read by humans in the raw file, keep the padding.
- Can I use bold, links or code spans inside a table cell?
- Yes — inline formatting works normally inside cells, so **bold**, a [link](https://example.com) and a `code span` all render. Block-level constructs do not: no headings, no lists, no fenced code blocks, no nested tables, because all of those need line breaks the row cannot contain. The trap is a code span containing a pipe, such as `ps | grep node`. Backticks do not protect a pipe from the table parser — the cell is split first and the code span is parsed afterwards, so you still have to write `ps \| grep node`. That is one of the few cases where you must escape by hand, because the converter only escapes pipes it can see in the source data.
- Why did my French CSV come out as a single column?
- Because the header line gave the sniffer nothing to count, and it is the header that decides which delimiters are even in the running. The tool tallies commas, semicolons, tabs and pipes outside quotes across the first few records, and a count that repeats across them beats one that is merely largest on line one — but a delimiter that never appears on the first record is not a candidate at all. A French or German spreadsheet exports with semicolons, since the comma is the decimal mark, and a header reading Nom;Prénom is detected correctly. A header that is a single word with no separator in it is not: there is nothing to count, the guess falls back to a comma, and the whole file arrives as one column. Set the Delimiter option to Semicolon by hand. The same fix applies to a tab-separated export pasted from a terminal, where the tabs may have been turned into spaces in transit and there is genuinely no delimiter left to find.
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 consists of cells whose only content are hyphens with optional leading or trailing colons; the header row must match the delimiter row in the number of cells or the table is not recognised; a table with no body rows generates no tbody
- GitHub Docs — Organizing information with tables: the pipe must be escaped as \| inside a cell, cells can carry inline formatting and links, and the vertical bars of a row need not line up
- RFC Editor — RFC 4180, Common Format and MIME Type for Comma-Separated Values (CSV) Files, October 2005 — section 2 rules 5 to 7: a field containing the delimiter, a line break or a double quote must be enclosed in double quotes, and an embedded double quote is written twice
- CommonMark — CommonMark Spec version 0.31.2 (2024-01-28): the core specification defines leaf and container blocks and contains no table construct — pipe tables are an extension, which is why a table that renders on GitHub may not render in a strict CommonMark processor
Spotted a mistake in this article?