---
title: "Pandoc grid tables in Markdown: syntax and alignment"
slug: markdown-grid-tables
excerpt: "A pandoc grid table draws borders with +, - and |, marks the header with =, and takes colons on that = line for alignment. Tested on pandoc 3.8.3."
quickAnswer: "In the pandoc 3.8.3 runs here, a grid table used + corners, - row lines and | cell walls, with a line of = under the header. Colons on that = line set alignment: on the right for right, on the left for left, on both for center. With no = line the table had no thead, and colons on the top border set alignment. One cell held an ordered list and a fenced code block, and another a block quote. Misdrawn lines still exited 0 with no warning, while the HTML merged cells, added columns or lost a row. The commonmark reader built no table."
keyTakeaways:
  - "The rows above the = line became thead rows, and an = line on each side of the last row wrote a tfoot."
  - "Colons on the = line gave 3 lines each of text-align right, left and center in the test file."
  - "With no = line, colons on the top border set alignment. With an = line, top-border colons wrote no text-align at all."
  - "Cells held an ordered list, a fenced code block and a block quote, each as its own HTML element."
  - "All 9 broken test files exited 0 under --fail-if-warnings, and a control file proved that flag can fail."
publishedDate: 2026-11-01
dateModified: 2026-11-01
author: Bobby Huang
cluster: B
role: post
status: published
faq:
  - q: How do I align a column in a pandoc grid table?
    a: "Put colons on the = line under the header. In the test, +======:+ gave right, +:=========+ gave left and +:=======:+ gave center, on the header and body cells alike."
  - q: Can a pandoc grid table have no header?
    a: "Yes. Leave out the = line. The test table came out with no thead element, and colons on its top border set its alignment."
  - q: Why did my grid table come out wrong with no error?
    a: "Pandoc exited 0 with no warning on all 9 broken test files. A | one column off became cell text, and a missing bottom border lost the last row. The awk command under Steps to check a grid table prints where each border character sits."
related:
  - markdown-table-merge-cells
  - markdown-table-multiline-cells
  - markdown-table-without-header
---

A pandoc grid table is drawn with `+` corners, `-` row lines and `|` cell walls, and a line of `=` marks where the header ends. Colons on that `=` line set each column's alignment. Get a line one column off and pandoc still builds something, with no warning, as the tests below show.

Every command here ran on a Mac with `pandoc --version` reporting `pandoc 3.8.3`, on small files read with `-f markdown`, and `-f commonmark` as a control. The checks stopped at the HTML pandoc wrote, so how any app displays it is out of scope, and so is any other Markdown tool. Cells that span columns or rows have their own post, [merged cells in grid tables](markdown-table-merge-cells.md), and text wrapped across lines has [multiline cells](markdown-table-multiline-cells.md). Neither is retested here.

## The border rules

Here is `cat gt-basic.md`, the working file:

```text
+----------+---------+
| Name     | Role    |
+==========+=========+
| Jane Doe | Owner   |
+----------+---------+
| John Doe | Editor  |
+----------+---------+
```

Every line is 22 characters wide, and the `+` and `|` marks sit at characters 1, 12 and 22 on all seven lines. The command `pandoc -f markdown -t html gt-basic.md` wrote:

```html
<table style="width:29%;">
<colgroup>
<col style="width: 15%" />
<col style="width: 13%" />
</colgroup>
<thead>
<tr>
<th>Name</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td>Jane Doe</td>
<td>Owner</td>
</tr>
<tr>
<td>John Doe</td>
<td>Editor</td>
</tr>
</tbody>
</table>
```

On the saved HTML, `grep -c '<table'` printed `1`, `grep -c '<tr'` printed `3` and `grep -c '<td'` printed `4`. The `style` and `colgroup` lines are looked at in [column width in Markdown tables](markdown-table-column-width.md).

The columns come from the `+` marks, and a `|` that lines up with no `+` is plain text. In `gt-bare-dash.md` the borders have no inner `+`:

```text
+--------------------+
| Name     | Role    |
+====================+
| Jane Doe | Owner   |
+--------------------+
```

That file got one column: `grep -c '<col '` printed `1`, against `2` for `gt-basic.html`. The cells came out as `<th>Name | Role</th>` and `<td>Jane Doe | Owner</td>`.

The commonmark reader did not build a table from `gt-basic.md`. Its extension list has no grid entry (`pandoc --list-extensions=commonmark | grep -c grid_tables` printed `0`; the control, `grep -c pipe_tables`, printed `1`), while the `markdown` list shows `+grid_tables` on line 24. The command `pandoc -f commonmark -t html --wrap=none gt-basic.md` wrote one paragraph:

```html
<p>+----------+---------+ | Name | Role | +==========+=========+ | Jane Doe | Owner | +----------+---------+ | John Doe | Editor | +----------+---------+</p>
```

Here `grep -c '<table'` printed `0`, against `1` for the `markdown` run.

## The = line marks the header

The rows above the `=` line went into `thead`. In `gt-two-headers.md` two rows sit above it:

```text
+----------+---------+
| Team     | Docs    |
+----------+---------+
| Name     | Role    |
+==========+=========+
| Jane Doe | Owner   |
+----------+---------+
```

Pandoc wrote both of them as header rows:

```html
<thead>
<tr>
<th>Team</th>
<th>Docs</th>
</tr>
<tr>
<th>Name</th>
<th>Role</th>
</tr>
</thead>
```

A second use of `=` wrote a footer. In `gt-foot.md` the last row has an `=` line on each side:

```text
+----------+---------+
| Name     | Hours   |
+==========+=========+
| Jane Doe | 3       |
+----------+---------+
| John Doe | 4       |
+==========+=========+
| Total    | 7       |
+==========+=========+
```

The Total row came out inside a `tfoot`, and `grep -c '<tfoot'` printed `1`:

```html
<tfoot>
<tr>
<td>Total</td>
<td>7</td>
</tr>
</tfoot>
```

## Align columns with colons on the = line

Colons go at the ends of each `=` run. Here is `cat gt-align.md`:

```text
+-------+----------+---------+
| Right | Left     | Center  |
+======:+:=========+:=======:+
| 12    | Jane Doe | x       |
+-------+----------+---------+
| 123   | John Doe | yy      |
+-------+----------+---------+
```

The cells pandoc wrote:

```html
<thead>
<tr>
<th style="text-align: right;">Right</th>
<th style="text-align: left;">Left</th>
<th style="text-align: center;">Center</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;">12</td>
<td style="text-align: left;">Jane Doe</td>
<td style="text-align: center;">x</td>
</tr>
<tr>
<td style="text-align: right;">123</td>
<td style="text-align: left;">John Doe</td>
<td style="text-align: center;">yy</td>
</tr>
</tbody>
```

A colon on the right end gave `right`, on the left end gave `left`, and on both gave `center`. Three greps on the saved file, for `text-align: right`, `text-align: left` and `text-align: center`, each printed `3`: one header cell and two body cells. A file with no colons, `gt-basic.md`, shows the other case: its cells carry no `style` at all. The same colon idea in a pipe table is covered in [Markdown tables](markdown-tables.md).

Colons elsewhere did not do the same job. With the colons moved to the top border of a table that also has an `=` line (`gt-top-colons.md`), `grep -c 'text-align'` printed `0`, and the table was otherwise normal.

On a row line between two body rows, they broke the table. Here is `gt-row-colons.md`:

```text
+-------+----------+---------+
| Right | Left     | Center  |
+=======+==========+=========+
| 12    | Jane Doe | x       |
+------:+:---------+:-------:+
| 123   | John Doe | yy      |
+-------+----------+---------+
```

Pandoc did not treat that line as a border. Its dashes landed inside the cells, which is easier to read with smart punctuation off, so this is the `tbody` from `pandoc -f markdown-smart -t html gt-row-colons.md`:

```html
<tbody>
<tr>
<td rowspan="2">12 ------: 123</td>
<td rowspan="2">Jane Doe :--------- John Doe</td>
<td rowspan="2">x :-------: yy</td>
</tr>
<tr>
</tr>
</tbody>
```

Two rows became one row of three `rowspan` cells and an empty `tr`. A count on the plain `markdown` run, `grep -c 'rowspan'`, printed `3`.

## A grid table with no header

Leave out the `=` line and the header goes with it. In `gt-noheader.md` every border uses dashes, and `grep -c '<thead'` printed `0`, against `1` for `gt-basic.html`. The two rows became four `td` cells. More on tables with no header in other forms is in [Markdown tables without a header](markdown-table-without-header.md).

With no `=` line, the colons move up to the top border. Here is `cat gt-noheader-align.md`:

```text
+---------:+:--------+
| Jane Doe | Owner   |
+----------+---------+
| John Doe | Editor  |
+----------+---------+
```

Pandoc wrote:

```html
<tbody>
<tr>
<td style="text-align: right;">Jane Doe</td>
<td style="text-align: left;">Owner</td>
</tr>
<tr>
<td style="text-align: right;">John Doe</td>
<td style="text-align: left;">Editor</td>
</tr>
</tbody>
```

All 4 cells took a `text-align`, and there is still no `thead`. So the same top-border colons wrote nothing in `gt-top-colons.md`, which has an `=` line, and set alignment here, where there is none.

## Block content in a cell

A cell can hold block elements, not just a line of text. [Multiline cells](markdown-table-multiline-cells.md) tests a bulleted list with a paragraph. This file tries a paragraph, an ordered list and a fenced code block in one cell, and a block quote in another:

````text
+--------+------------------------+
| Step   | Detail                 |
+========+========================+
| Build  | Run these in order:    |
|        |                        |
|        | 1. Install             |
|        | 2. Test                |
|        |                        |
|        | ```sh                  |
|        | pnpm   test            |
|        | ```                    |
+--------+------------------------+
| Review | > Read the diff first. |
+--------+------------------------+
````

The body from `pandoc -f markdown -t html gt-blocks.md`:

```html
<tbody>
<tr>
<td>Build</td>
<td><p>Run these in order:</p>
<ol type="1">
<li>Install</li>
<li>Test</li>
</ol>
<div class="sourceCode" id="cb1"><pre
class="sourceCode sh"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">pnpm</span>   test</span></code></pre></div></td>
</tr>
<tr>
<td>Review</td>
<td><blockquote>
<p>Read the diff first.</p>
</blockquote></td>
</tr>
</tbody>
```

Greps for `<ol`, `<pre` and `<blockquote` each printed `1`. The three spaces in `pnpm   test` survived inside the code block.

## When a line is off by one column

Each file below is `gt-basic.md` cut down to one body row, with one thing wrong. None of them failed. All came back with exit code 0.

A `|` one column to the right, in `gt-off-pipe.md`:

```text
| Jane Doe  | Owner  |
```

That `|` no longer lines up with a `+`, so it became text, and the two cells merged into one:

```html
<td colspan="2">Jane Doe | Owner</td>
```

A `+` one column to the right in the bottom border, in `gt-off-plus.md`:

```text
+-----------+--------+
```

This one changed the shape of the whole table. The `grep -c '<col '` count went to `3` for a table drawn with two columns, and the body row and header both picked up spans:

```html
<colgroup>
<col style="width: 15%" />
<col style="width: 1%" />
<col style="width: 12%" />
</colgroup>
<thead>
<tr>
<th>Name</th>
<th colspan="2">Role</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="3">Jane Doe | Owner</td>
</tr>
</tbody>
```

A row one character short, in `gt-short-row.md`, where the last `|` sits at character 21 instead of 22:

```text
| Jane Doe | Owner  |
```

The closing wall turned into cell text:

```html
<td>Owner |</td>
```

A row one character long, in `gt-long-row.md`, with `| Jane Doe | Owner    |`, added a third column. Its `<col ` count was `3`, with an empty header cell:

```html
<th>Name</th>
<th>Role</th>
<th></th>
```

An `=` line one character short, in `gt-short-sep.md`, ending `+========+` where the second column needs nine. With smart punctuation off so the dashes stay as typed, the table had two columns and this header cell:

```html
<th>Role --------+ Owner</th>
```

The word Owner and dashes from the bottom border ended up in the header cell, and Owner still appeared in its body cell too.

Two missing lines did more damage. With the top border gone (`gt-no-top.md`), `grep -c '<table'` printed `0`:

```html
<div class="line-block">Name | Role |</div>
<p>+==========+=========+ | Jane Doe | Owner |
+----------+---------+</p>
```

That output is from the smart-off run. With the bottom border gone (`gt-no-bottom.md`), the table was built and the row was lost. The `tbody` came out empty, `grep -c '<td'` printed `0`, and `grep -c 'Jane Doe'` printed `0`, against `1` on `gt-basic.html`:

```html
<tbody>
</tbody>
```

No run warned about any of this. The flag `--fail-if-warnings` left all 9 broken files at exit code 0: the seven in this section, plus `gt-row-colons.md` and `gt-bare-dash.md`. To show the flag can fire, a control file with a duplicate link reference went through the same command. It printed `Failing because there were warnings.` and returned exit code 3. So the exit code told nothing about these tables.

## Steps to check a grid table

Each command ran on the files above. Put your own file's name in place of them.

1. Print where each border character sits, one line per pattern:

   ```sh
   awk '{ s=""; for (i=1; i<=length; i++) { c=substr($0,i,1); if (c=="+" || c=="|") s=s" "i }; print s }' gt-basic.md | sort | uniq -c
   ```

   On `gt-basic.md` it printed one pattern, `7  1 12 22`.
2. Run the same command on the suspect file. `gt-off-pipe.md` printed a second pattern, `1  1 13 22`, and `gt-short-row.md` printed `1  1 12 21`. A pattern with a count of 1 is the line to look at.
3. Allow for spans you meant to draw. A deliberate wide cell in `gt-span.md` also printed a second pattern, `1  1 22`.
4. Save the HTML with `pandoc -f markdown -t html gt-basic.md -o gt-basic.html`.
5. Count columns with `grep -c '<col ' gt-basic.html`. It printed `2`, the number drawn. The `gt-long-row.md` file printed `3`.
6. Count spans with `grep -c 'colspan' gt-basic.html`. It printed `0`. The `gt-off-pipe.md` file, which draws none on purpose, printed `1`.
7. Check for a lost row by grepping for text you know is in it. `grep -c 'Jane Doe' gt-no-bottom.html` printed `0`.

## Where Sashmark fits

Sashmark is a Markdown reader and editor for people whose AI agents write Markdown. It is in development; nothing has been released. There are no AI features in the app.
