GFM Pipes, CSS, and One Astro Component
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
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:
- A clean authoring experience — write tables in markdown, not JSX
- Zero client-side JavaScript — tables are content, not applications
- Responsive behavior — tables that work on mobile without breaking the layout
- Theme-aware styling — tables that adapt to light and dark modes
- 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:
| Left | Center | Right |
|---|---|---|
| text | text | text |
| longer text | longer text | longer 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:
| Step | Action | Result |
|---|---|---|
| 1 | Install Astro | Project scaffolded |
| 2 | Write MDX | Content ready |
| 3 | Add prose class | Tables 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:
| What | Where | Command |
|---|---|---|
| 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 server | Remote (~/.zshrc) | export PULSE_SERVER='tcp:127.0.0.1:4713' |
| Fix ALSA | Remote (~/.asoundrc) | pcm.!default { type pulse } |
| Install plugins | Remote | sudo 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:
<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:
| Step | Tool | Input | Output | Duration | Status | Notes |
|---|---|---|---|---|---|---|
| 1 | Compiler | src/main.rs | target/release/app | 12s | Pass | Optimized build |
| 2 | Linter | src/**/*.rs | 3 warnings | 2s | Warn | Non-critical |
| 3 | Tester | tests/ | 47/47 passed | 8s | Pass | Full suite |
| 4 | Bundler | dist/ | 2.3 MB | 4s | Pass | Gzipped |
| 5 | Deploy | bundle.tar.gz | prod-server | 31s | Pass | Zero downtime |
And here is a 15-column stress test — this is where the scroll wrapper earns its keep:
| A | B | C | D | E | F | G | H | I | J | K | L | M | N | O |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
val | val | val | val | val | val | val | val | val | val | val | val | val | val | val |
| alpha | beta | gamma | delta | epsilon | zeta | eta | theta | iota | kappa | lambda | mu | nu | xi | omicron |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
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
| Concern | Solution | Cost |
|---|---|---|
| Authoring | GFM pipe tables | Zero — built into Astro |
| Base styling | Tailwind Typography prose | Zero — already using Tailwind |
| Long content | word-break + overflow-wrap | CSS only |
| Readable columns | min-width: 8ch on cells | CSS only |
| Wide tables | .table-wrapper with overflow-x: auto | 1 small Astro component |
| Header typography | font-family: var(--font-primary) on th | CSS only |
| Theme support | --tw-prose-* CSS custom properties | Zero — inherited from prose |
| JavaScript | None | None 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