Skip to content

Markdown Table Without a Header (and Table Captions)

By MarkdownTables.com editorial teamUpdated

On this page

How do I make a Markdown table without a header?

A GFM table is recognised by a header row followed by a delimiter row, and both must have the same number of cells. There is no way to skip the header, but the header cells can be empty. That is the standard workaround on GitHub, GitLab, VS Code, Jupyter and every other GFM-based renderer:

Key–value table with an empty header row
Markdown
|              |                     |
| ------------ | ------------------- |
| **Version**  | 2.4.1               |
| **License**  | MIT                 |
| **Node**     | 20 or later         |
| **Homepage** | https://example.com |
Preview
Version2.4.1
LicenseMIT
Node20 or later
Homepagehttps://example.com

GitHub renders a short empty header row above the data. Obsidian shows it as a full-height blank row.

Open in editor : Key–value table with an empty header row

Some people fill the header with   instead of spaces. The output is the same: an empty-looking header row. Use whichever survives your formatter.

The same workaround with   header cells
Markdown
|   |   |
| :----- | :----- |
| Host   | db.internal:5432 |
| Pool   | 20 connections   |
Preview
  
Hostdb.internal:5432
Pool20 connections

Before reaching for a workaround, ask whether the table really has no header. A two-column settings table almost always has one (“Setting | Value”, “Field | Description”). A short real header costs one line, renders identically everywhere and gives screen-reader users context for every cell, which blank headers don't.

Usually the better fix: a short, real header
Markdown
| Setting  | Value            |
| :------- | :--------------- |
| Host     | db.internal:5432 |
| Pool     | 20 connections   |
| SSL mode | `require`        |
Preview
SettingValue
Hostdb.internal:5432
Pool20 connections
SSL moderequire

Every way to get a headerless table

Ways to make a table without a visible header
MethodSyntaxWorks onResult
Empty header cells| | | then | --- | --- |All GFM renderersThin blank header row stays in the HTML
  header cells|   |   |All GFM renderersSame as empty cells
Real short header| Setting | Value |EverywhereNormal header; most accessible
Omit the separator lineRows only, each starting with |kramdown (Jekyll, GitHub Pages)True headerless <tbody> table
Simple, multiline or grid tableDashed or +---+ lines, no headerPandoc, Quarto, R MarkdownTrue headerless table
HTML table<table><tbody>…</tbody></table>Anywhere raw HTML is allowedTrue headerless table
Hide empty header with CSSthead:not(:has(th:not(:empty)))Sites where you control CSSHeader in HTML, hidden on screen

True headerless tables: kramdown, Pandoc and HTML

kramdown (Jekyll and GitHub Pages)

In kramdown, a row only becomes a header when a separator line follows it. Leave the separator out and every row is a body row. Start each row with a pipe. This is the syntax to use on GitHub Pages sites, which build with Jekyll and kramdown, but note that the same file viewed on github.com is parsed as GFM, where these lines are not a table.

kramdown: no separator line, no header
Markdown
| Host | db.internal:5432 |
| Pool | 20 connections   |
| SSL  | require          |
Preview
Hostdb.internal:5432
Pool20 connections
SSLrequire

Preview shows kramdown output. GitHub’s file view renders these lines as plain text.

Pandoc, Quarto and R Markdown

Pandoc's pipe tables need a header, just like GFM; the Pandoc manual suggests blank header cells to simulate a headerless one. Its other three styles can drop the header natively. A simple table without a header must start and end with a dashed line:

Pandoc simple table without a headerMarkdown
-------  ----------------
Host     db.internal:5432
Pool     20 connections
SSL      require
-------  ----------------

Multiline tables work the same way, and a grid table is headerless when it has no +===+ line:

Pandoc grid table without a headerMarkdown
+------+------------------+
| Host | db.internal:5432 |
+------+------------------+
| Pool | 20 connections   |
+------+------------------+

HTML: a table with only a body

Wherever raw HTML is allowed (GitHub, GitLab, Obsidian, VS Code, Jupyter, most static site generators), a table without <thead> has no header row at all. Marking the label cells as <th scope="row"> keeps them bold and tells assistive technology they label their row.

Headerless key–value table in HTMLHTML
<table>
  <tbody>
    <tr>
      <th scope="row">Host</th>
      <td><code>db.internal:5432</code></td>
    </tr>
    <tr>
      <th scope="row">Pool</th>
      <td>20 connections</td>
    </tr>
  </tbody>
</table>

MkDocs, Docusaurus and Hugo: hide the empty header with CSS

MkDocs (Python-Markdown), Docusaurus (remark-gfm) and Hugo (Goldmark) all follow the GFM rule, so they need the empty header row too. Because you control the site's CSS, you can hide that row. The selector below matches a <thead> in which no header cell has content, so normal tables are untouched:

Hide empty header rowsCSS
/* docs/stylesheets/extra.css — hide a header row whose cells are all empty */
table thead:not(:has(th:not(:empty))) {
  display: none;
}
Register the stylesheet in MkDocsYAML
# mkdocs.yml
extra_css:
  - stylesheets/extra.css

Use empty cells (|  |), not &nbsp;, with this rule: a cell containing a non-breaking space is not :empty. In Docusaurus add the rule to src/css/custom.css; in Hugo, to your theme's stylesheet. For Python-Markdown there are also third-party extensions, such as markdown-tables-extended, that accept tables without a header.

How do I add a caption to a Markdown table?

GFM has no caption syntax. Each toolchain that does support captions uses its own, so pick the one your renderer understands:

Table caption syntax by tool
ToolCaption syntaxPlacement
GitHub, GitLab, VS CodeBold line or heading; or HTML <caption>Above the table
Pandoc / R MarkdownTable: Caption or : CaptionOwn paragraph before or after the table
Quarto: Caption {#tbl-id}, or tbl-cap cell optionAfter the table; @tbl-id cross-references
R (knitr)knitr::kable(df, caption = "…")Generated by the chunk
MultiMarkdown[Caption] or [Caption][label]Line directly above or below
MkDocs + PyMdown Extensions/// table-caption blockAfter the table
HTML anywhere<caption> as the first child of <table>Rendered above by default

GitHub and other GFM renderers

Put the caption in a bold line or a small heading directly above the table. It isn't tied to the table in the HTML, but it reads correctly and works everywhere.

A bold line as a caption
Markdown
**Table 1.** Response time by region (p95, ms)

| Region | p95 |
| :----- | --: |
| EU     | 120 |
| US     | 95  |
Preview
Regionp95
EU120
US95

For a real <caption> element, which screen readers announce with the table, write the table in HTML. GitHub keeps the element.

HTML table with a captionHTML
<table>
  <caption>Table 1. Response time by region (p95, ms)</caption>
  <thead>
    <tr><th>Region</th><th>p95</th></tr>
  </thead>
  <tbody>
    <tr><td>EU</td><td>120</td></tr>
    <tr><td>US</td><td>95</td></tr>
  </tbody>
</table>

Pandoc, R Markdown and Quarto

Pandoc reads a paragraph that starts with Table: (or just :) next to a table as its caption. The same syntax works in R Markdown documents, which are rendered by Pandoc.

Pandoc captionMarkdown
| Region | p95 |
| :----- | --: |
| EU     | 120 |
| US     | 95  |

Table: Response time by region (p95, ms)

Quarto adds a label in braces so the table is numbered and can be cross-referenced:

Quarto caption with a cross-reference labelMarkdown
| Region | p95 |
| :----- | --: |
| EU     | 120 |
| US     | 95  |

: Response time by region (p95, ms) {#tbl-latency}

See @tbl-latency for the numbers.

For a table produced by code, give the caption to the function or the cell. In R Markdown use knitr::kable()'s caption argument; in Quarto use the tbl-cap cell option with a tbl- label.

Caption from knitr::kable()R Markdown
```{r latency}
knitr::kable(latency, caption = "Response time by region (p95, ms)")
```
Caption from a Quarto cell optionQuarto
```{r}
#| label: tbl-latency
#| tbl-cap: "Response time by region (p95, ms)"
knitr::kable(latency)
```

MultiMarkdown and MkDocs

MultiMarkdown takes a caption in square brackets on the line directly above or below the table, with an optional label for links:

MultiMarkdown captionMarkdown
| Region | p95 |
| :----- | --: |
| EU     | 120 |
| US     | 95  |
[Response time by region][latency]

In MkDocs, enable pymdownx.blocks.caption from PyMdown Extensions and put a caption block after the table. It wraps the table in a <figure> with a <figcaption>.

MkDocs caption blockMarkdown
| Region | p95 |
| :----- | --: |
| EU     | 120 |
| US     | 95  |

/// table-caption
Response time by region (p95, ms)
///

Header column: making the left column a header

Searches for a “header column” or “left column header” usually mean a table whose first column labels each row, like a pricing comparison. GFM has no syntax for row headers. The common approach is to bold the first column:

Bold first column as a visual row header
Markdown
| Plan     | Free | Pro   |
| :------- | :--: | :---: |
| **Seats**    | 1    | 10    |
| **Storage**  | 1 GB | 1 TB  |
| **Support**  | —    | Email |
Preview
PlanFreePro
Seats110
Storage1 GB1 TB
Support—Email

Bold only changes the look; the cells are still <td>, so a screen reader doesn't announce them as headers. When accessibility matters, write the table in HTML with <th scope="row"> in each row (as in the HTML example above). If the labels belong on top, swap rows and columns with the table transposer.

Headerless tables and captions by platform

Headerless tables and captions by platform
PlatformEmpty-header workaroundTrue headerlessCaption
GitHub (GFM)An empty header renders as a thin blank header row.YesPartly: HTML <tbody> tablePartly: HTML <caption>
GitLab (GLFM)YesPartly: HTML tablePartly: HTML <caption>
ObsidianThe empty header shows as a visible blank row.Partly: Blank row visiblePartly: HTML tablePartly: HTML <caption>
VS Code previewYesPartly: HTML tablePartly: HTML <caption>
JupyterYesPartly: HTML tablePartly: HTML <caption>
Pandoc / Quarto / R MarkdownPipe tables still need a header; the other three table styles do not.YesYes: Simple, multiline, gridYes: Table: or : line
Jekyll / GitHub Pages (kramdown)YesYes: Omit the separator linePartly: HTML <caption>
MkDocs (Python-Markdown)Captions need PyMdown Extensions (pymdownx.blocks.caption).YesPartly: CSS or HTMLPartly: /// table-caption
Docusaurus / HugoHugo drops raw HTML unless markup.goldmark.renderer.unsafe is true.YesPartly: CSS or HTMLPartly: HTML <caption>
MultiMarkdownYesPartly: Header expectedYes: [Caption]
NotionImported tables become Notion tables with “Header row” and “Header column” toggles.Not applicableYes: Turn off “Header row”Not applicable
RedditYesNoNo

✅ supported · ⚠️ partly or with a workaround · ❌ not supported · Last verified October 2026

“HTML table” means writing the whole table in HTML, which works where the renderer allows raw HTML.

Common mistakes and fixes

Starting with the delimiter row

Deleting the header line leaves the delimiter row on top. GFM then finds no table and shows the pipes as text.

Broken: no header row (renders as plain text)Markdown
| --- | --- |
| Host | db.internal:5432 |
| Pool | 20 connections   |
Fixed: an empty header row above the delimiter
Markdown
|              |                     |
| ------------ | ------------------- |
| **Version**  | 2.4.1               |
| **License**  | MIT                 |
| **Node**     | 20 or later         |
| **Homepage** | https://example.com |
Preview
Version2.4.1
LicenseMIT
Node20 or later
Homepagehttps://example.com

One empty header cell for a two-column table

| | is a single header cell. The delimiter row has two, so the counts don't match and nothing renders. Give the header one empty cell per column.

Broken: 1 header cell, 2 delimiter cellsMarkdown
| |
| --- | --- |
| Host | db.internal:5432 |

Caption syntax on the wrong renderer

Table: …, : … and […] are not GFM. On GitHub they render as a stray paragraph, and [Caption] directly under a table becomes an extra row. Use a bold line there instead.

Markdown inside an HTML table

Inside <td>, **bold** and backticks are only processed if blank lines separate them from the tags. Use <b> and <code> instead. Check structure problems with the table validator.

When to switch to HTML

Stay with pipe tables and an empty or short header for READMEs and docs people edit as text. Switch to HTML when you need:

  • no header row at all, on a GFM renderer
  • a real <caption> or row headers (<th scope="row">) for accessibility
  • merged cells as well (see merged cells)

The Markdown to HTML converter gives you a starting point to edit. For the rest of the table rules, see Markdown table syntax; to keep long key–value tables out of the way, see collapsible tables, and for widths see column width.

Frequently asked questions

Can you make a Markdown table without a header on GitHub?

Not truly. GitHub Flavored Markdown requires a header row followed by a delimiter row. Leave the header cells empty (| | |) and GitHub draws a thin blank header row. For no header at all, write an HTML <table> with only a <tbody>, which GitHub renders.

How do I remove the header row from a table in Obsidian?

You can’t with pipe syntax: Obsidian needs a header and shows empty header cells as a visible blank row. Use an HTML table without <thead>, or add a CSS snippet that hides empty header rows. Often a short real header such as “Field | Value” is the cleaner fix.

How do I make a table without a header in MkDocs?

Write an empty header row, then hide it with CSS in extra_css: table thead:not(:has(th:not(:empty))) { display: none; }. Python-Markdown’s tables extension always needs a header row; third-party extensions such as markdown-tables-extended add headerless tables.

How do I add a caption to a table in R Markdown?

For a generated table, pass caption to kable: knitr::kable(df, caption = "My caption"). For a hand-written pipe table, add a line Table: My caption after it, separated by a blank line. In Quarto use : My caption {#tbl-id} or the tbl-cap cell option.

Does GitHub support table captions?

Not in Markdown. Pandoc’s Table: line and MultiMarkdown’s [Caption] show up as plain text on GitHub. Put a bold line or heading above the table, or write the table in HTML with a <caption> element, which GitHub keeps.

How do I make the first column a header in a Markdown table?

GFM has no row-header syntax. Bold the first column (**Seats**) for the look, or use an HTML table with <th scope="row"> in each row so screen readers treat those cells as headers. Notion tables have a “Header column” toggle.

Can a Markdown table have the header on the left?

Not in pipe syntax: the header is always the top row. Either transpose the data so the labels become the header row, or use an HTML table with <th scope="row"> cells down the left side. The table transposer swaps rows and columns for you.

Build the table in a grid and copy clean Markdown, header row included.

Open the table generator