Tables in MDX Without External Libraries

GFM Pipes, CSS, and One Astro Component

09.04.2026 | 21 Shawwal 1447
5 min read

بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ

Why This Matters

You are writing blog posts in MDX. You need tables. The internet offers you TanStack Table, AG Grid, shadcn/ui Table, and a dozen npm packages — all designed for interactive data grids with sorting, filtering, and pagination.

You have none of those requirements. You have static content. Rows and columns of text and code. What you actually need is:

  1. A clean authoring experience — write tables in markdown, not JSX
  2. Zero client-side JavaScript — tables are content, not applications
  3. Responsive behavior — tables that work on mobile without breaking the layout
  4. Theme-aware styling — tables that adapt to light and dark modes
  5. Long content handling — file paths and CLI commands that don’t blow out column widths

Here is how to get all five with zero external libraries. Every table in this post is rendered using the approach described — what you see is the proof.

The Authoring Experience

GFM (GitHub Flavored Markdown) pipe tables are built into Astro’s MDX pipeline via remark-gfm. No install needed. You write this:

| What | Where | Command |
|------|-------|---------|
| TCP module | Local | Add `"tcp:127.0.0.1:4713"` to `server.address` |
| SSH tunnel | Local (`~/.ssh/config`) | `RemoteForward 4713 127.0.0.1:4713` |

And Astro renders a real <table> with <thead>, <tbody>, <tr>, <th>, <td>. No imports, no component wrappers in the content file.

GFM also supports column alignment via the separator row — :--- for left, :---: for center, ---: for right:

LeftCenterRight
texttexttext
longer textlonger textlonger text

What You Get for Free

If you are on Tailwind CSS, you are likely using @tailwindcss/typography. The prose class styles every table inside your article automatically — padding, borders, header weight, color inheritance. Combined with GFM, this covers most use cases out of the box.

Here is a basic table:

StepActionResult
1Install AstroProject scaffolded
2Write MDXContent ready
3Add prose classTables styled

No configuration. No CSS written. It just works.

The Problem: Long Content in Cells

The moment you put real content in table cells — file paths, CLI commands, configuration strings — the default browser behavior fights you. Without intervention, the browser sizes columns based on content width. A cell with ~/.config/pipewire/pipewire-pulse.conf demands more horizontal space than your article container offers.

Here is a table with real-world content — paths, flags, inline code. Resize your browser window to see how it behaves:

WhatWhereCommand
TCP module (permanent)Local (~/.config/pipewire/pipewire-pulse.conf)Add "tcp:127.0.0.1:4713" to server.address
SSH tunnel (permanent)Local (~/.ssh/config)RemoteForward 4713 127.0.0.1:4713 under Host your-server
Set audio serverRemote (~/.zshrc)export PULSE_SERVER='tcp:127.0.0.1:4713'
Fix ALSARemote (~/.asoundrc)pcm.!default { type pulse }
Install pluginsRemotesudo apt install libasound2-plugins pulseaudio-utils

The Fix: CSS Only

A handful of CSS rules solve this. No JavaScript. No external libraries. Add them to your prose stylesheet:

/* Wrapper — injected via component override (explained below) */
& .table-wrapper {
overflow-x: auto;
}
& table {
min-width: 100%;
word-break: break-word;
}
& th {
font-family: var(--font-primary);
}
& td, & th {
min-width: 8ch;
overflow-wrap: anywhere;
}

What Each Rule Does

min-width: 100% — the table fills the prose container at minimum. If column minimums push it wider, the table grows — and the wrapper scrolls the excess.

word-break: break-word — long unbroken strings like file paths break mid-word to wrap within the cell instead of stretching it.

overflow-wrap: anywhere — catches inline elements like <code> spans that word-break alone misses. Your backtick-wrapped commands break properly.

min-width: 8ch on cells — prevents columns from collapsing to unreadable widths. When many columns push the total past the container, the table grows wider and the wrapper scrolls. Without this, a 15-column table would crush each cell to a few pixels.

font-family: var(--font-primary) on <th> — table headers use the same serif font as headings. Small detail, big consistency.

.table-wrapper with overflow-x: auto — the safety net. If a table has too many columns to fit even after word breaking (scroll the 15-column table below to see), the wrapper scrolls horizontally instead of bleeding into adjacent layout areas.

The Component Override

The scroll wrapper needs a <div> around the <table>. CSS cannot inject parent elements. But Astro’s MDX components prop can — without changing a single MDX file.

Create one component:

Table_Wrapper.astro
<div class="table-wrapper">
<table><slot /></table>
</div>

Add it to your components map in the page route:

export const components = {
h1: Custom_Header_Lvl_1,
h2: Custom_Header_Lvl_2,
table: Table_Wrapper, // this line
}

Authors keep writing | … |. The wrapper is invisible to them. Every pipe table in every MDX file gets the wrapper automatically.

Wide Table Test: Many Columns

Here is a 7-column table:

StepToolInputOutputDurationStatusNotes
1Compilersrc/main.rstarget/release/app12sPassOptimized build
2Lintersrc/**/*.rs3 warnings2sWarnNon-critical
3Testertests/47/47 passed8sPassFull suite
4Bundlerdist/2.3 MB4sPassGzipped
5Deploybundle.tar.gzprod-server31sPassZero downtime

And here is a 15-column stress test — this is where the scroll wrapper earns its keep:

ABCDEFGHIJKLMNO
valvalvalvalvalvalvalvalvalvalvalvalvalvalval
alphabetagammadeltaepsilonzetaetathetaiotakappalambdamunuxiomicron
123456789101112131415

On desktop, word breaking keeps reasonable tables contained. On mobile or with extreme column counts, the wrapper scrolls horizontally — content stays inside the article, never bleeds into the sidebar.

What Ships to the Browser

Everything compiles at build time. The rendered output is static HTML and CSS — no hydration, no framework island:

<div class="table-wrapper">
<table>
<thead><tr><th>What</th><th>Where</th></tr></thead>
<tbody><tr><td>Value</td><td>Value</td></tr></tbody>
</table>
</div>

The Full Picture

ConcernSolutionCost
AuthoringGFM pipe tablesZero — built into Astro
Base stylingTailwind Typography proseZero — already using Tailwind
Long contentword-break + overflow-wrapCSS only
Readable columnsmin-width: 8ch on cellsCSS only
Wide tables.table-wrapper with overflow-x: auto1 small Astro component
Header typographyfont-family: var(--font-primary) on thCSS only
Theme support--tw-prose-* CSS custom propertiesZero — inherited from prose
JavaScriptNoneNone shipped

For static content tables, the stack already has what you need.

Aerospace engineer

Ethical entrepreneur in public

You handle your business

I handle the digital side

Work with me
  • AI with honesty
  • Private infrastructure
  • Websites that perform

Tell me about your situation:

javed@javedab.com More about me