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:
| | |
| ------------ | ------------------- |
| **Version** | 2.4.1 |
| **License** | MIT |
| **Node** | 20 or later |
| **Homepage** | https://example.com || Version | 2.4.1 |
| License | MIT |
| Node | 20 or later |
| Homepage | https://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 rowSome people fill the header with instead of spaces. The output is the same: an empty-looking header row. Use whichever survives your formatter.
header cells| | |
| :----- | :----- |
| Host | db.internal:5432 |
| Pool | 20 connections || Host | db.internal:5432 |
| Pool | 20 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.
| Setting | Value |
| :------- | :--------------- |
| Host | db.internal:5432 |
| Pool | 20 connections |
| SSL mode | `require` || Setting | Value |
|---|---|
| Host | db.internal:5432 |
| Pool | 20 connections |
| SSL mode | require |
Every way to get a headerless table
| Method | Syntax | Works on | Result |
|---|---|---|---|
| Empty header cells | | | | then | --- | --- | | All GFM renderers | Thin blank header row stays in the HTML |
header cells | | | | | All GFM renderers | Same as empty cells |
| Real short header | | Setting | Value | | Everywhere | Normal header; most accessible |
| Omit the separator line | Rows only, each starting with | | kramdown (Jekyll, GitHub Pages) | True headerless <tbody> table |
| Simple, multiline or grid table | Dashed or +---+ lines, no header | Pandoc, Quarto, R Markdown | True headerless table |
| HTML table | <table><tbody>…</tbody></table> | Anywhere raw HTML is allowed | True headerless table |
| Hide empty header with CSS | thead:not(:has(th:not(:empty))) | Sites where you control CSS | Header 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.
| Host | db.internal:5432 |
| Pool | 20 connections |
| SSL | require || Host | db.internal:5432 |
| Pool | 20 connections |
| SSL | require |
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:
------- ----------------
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:
+------+------------------+
| 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.
<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:
/* docs/stylesheets/extra.css — hide a header row whose cells are all empty */
table thead:not(:has(th:not(:empty))) {
display: none;
}# mkdocs.yml
extra_css:
- stylesheets/extra.cssUse empty cells (| |), not , 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:
| Tool | Caption syntax | Placement |
|---|---|---|
| GitHub, GitLab, VS Code | Bold line or heading; or HTML <caption> | Above the table |
| Pandoc / R Markdown | Table: Caption or : Caption | Own paragraph before or after the table |
| Quarto | : Caption {#tbl-id}, or tbl-cap cell option | After 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 block | After 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.
**Table 1.** Response time by region (p95, ms)
| Region | p95 |
| :----- | --: |
| EU | 120 |
| US | 95 || Region | p95 |
|---|---|
| EU | 120 |
| US | 95 |
For a real <caption> element, which screen readers announce with the table, write the table in HTML. GitHub keeps the element.
<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.
| 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:
| 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.
```{r latency}
knitr::kable(latency, caption = "Response time by region (p95, ms)")
``````{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:
| 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>.
| 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:
| Plan | Free | Pro |
| :------- | :--: | :---: |
| **Seats** | 1 | 10 |
| **Storage** | 1 GB | 1 TB |
| **Support** | — | Email || Plan | Free | Pro |
|---|---|---|
| Seats | 1 | 10 |
| Storage | 1 GB | 1 TB |
| Support | — |
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
| Platform | Empty-header workaround | True headerless | Caption |
|---|---|---|---|
| GitHub (GFM)An empty header renders as a thin blank header row. | Yes | Partly: HTML <tbody> table | Partly: HTML <caption> |
| GitLab (GLFM) | Yes | Partly: HTML table | Partly: HTML <caption> |
| ObsidianThe empty header shows as a visible blank row. | Partly: Blank row visible | Partly: HTML table | Partly: HTML <caption> |
| VS Code preview | Yes | Partly: HTML table | Partly: HTML <caption> |
| Jupyter | Yes | Partly: HTML table | Partly: HTML <caption> |
| Pandoc / Quarto / R MarkdownPipe tables still need a header; the other three table styles do not. | Yes | Yes: Simple, multiline, grid | Yes: Table: or : line |
| Jekyll / GitHub Pages (kramdown) | Yes | Yes: Omit the separator line | Partly: HTML <caption> |
MkDocs (Python-Markdown)Captions need PyMdown Extensions (pymdownx.blocks.caption). | Yes | Partly: CSS or HTML | Partly: /// table-caption |
Docusaurus / HugoHugo drops raw HTML unless markup.goldmark.renderer.unsafe is true. | Yes | Partly: CSS or HTML | Partly: HTML <caption> |
| MultiMarkdown | Yes | Partly: Header expected | Yes: [Caption] |
| NotionImported tables become Notion tables with “Header row” and “Header column” toggles. | Not applicable | Yes: Turn off “Header row” | Not applicable |
| Yes | No | No |
✅ 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.
| --- | --- |
| Host | db.internal:5432 |
| Pool | 20 connections || | |
| ------------ | ------------------- |
| **Version** | 2.4.1 |
| **License** | MIT |
| **Node** | 20 or later |
| **Homepage** | https://example.com || Version | 2.4.1 |
| License | MIT |
| Node | 20 or later |
| Homepage | https://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.
| |
| --- | --- |
| 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