Responsive Tables for Technical Docs That Don’t Break in Production

Was this helpful?

You ship a neat API reference table. It looks perfect on your 32-inch monitor. Then support forwards a screenshot from a phone where the table
has escaped the page like a feral cat, pushing your navigation off-screen and making the “Copy” buttons unclickable.

Tables are the silent failure mode of technical docs: they work until they don’t, and when they don’t, they take the rest of your layout with them.
This is a practical, ops-grade way to build responsive tables: a scroll container that behaves, sticky headers that don’t jitter, and code cells that remain readable.

Non-negotiable principles: what “responsive table” actually means

“Responsive table” is not “make it smaller until it fits.” In technical docs, tables carry dense, high-stakes information:
parameters, defaults, compatibility matrices, exit codes, storage limits, and the kind of footnotes that prevent weekend pages.
Your job is to keep the table truthful and usable across viewports and input methods.

Principle 1: Don’t reflow truth into fiction

A lot of “responsive table” recipes collapse columns into stacked cards. That can work for marketing features.
In docs, it often turns comparisons into a scavenger hunt. If the reader needs to compare values across columns, preserve the grid.
Horizontal scroll is acceptable when it’s intentional and doesn’t break everything else.

Principle 2: The table is not allowed to escape its container

The top failure mode is overflow: unbroken strings (UUIDs, base64, SHA256, file paths, commands) force the table wider than the viewport.
Once that happens, your layout gets “creative.” Fix it at the container level and at the cell level. Both.

Principle 3: Sticky headers must stay aligned with columns

Sticky headers are a productivity feature—until they drift out of alignment due to inconsistent widths, border models, or nested scroll contexts.
A sticky header that doesn’t match its column is worse than no sticky header. It turns reading into guesswork.

Principle 4: Code cells are not prose

Inline code is often long, copy-pasted, and read by scanning. It needs monospace, sensible wrapping rules, and a background that works in dark mode.
“Just let it wrap” is not a strategy; it’s how you get broken tokens (and broken deploys).

Principle 5: If it’s not keyboard-accessible, it’s not done

Scroll containers can trap focus, sticky headers can overlap focus outlines, and copy buttons can become unreachable.
Make sure there’s a visible focus state and a stable scroll experience for touch and keyboard.

A quote I use when folks try to “ship it and see” with doc UI: Hope is not a strategy. — General Gordon R. Sullivan

Joke #1: Sticky headers are like on-call rotations: great until they drift, and then everyone argues about whose fault it is.

A few facts and history (because the web has receipts)

  • HTML tables predate CSS layouts and were used for page layout in the 1990s; the backlash is why table styling is still quirky in edge cases.
  • position: sticky was designed to reduce JavaScript scroll handlers—a performance win that became essential for sticky table headers and sidebars.
  • Mobile browsers historically handled overflow and scroll chaining differently, which is why “works on desktop” never proved “works on phone.”
  • Long tokens became common in docs because of modern infra: container digests, cloud resource IDs, JWTs, checksums, and tracing IDs are built to be unbroken strings.
  • Copy-to-clipboard UI patterns got popular in docs with the rise of DevOps tooling; they’re now expected for commands and config snippets—even inside tables.
  • The “table-layout: fixed” trick is older than many design systems, and it’s still one of the best levers for predictable column widths under stress.
  • CLS (Cumulative Layout Shift) became a formal metric with Core Web Vitals; tables with web fonts and late-loading content are frequent CLS offenders.
  • Many doc sites are static builds (SSG), but tables can still “render slow” due to client-side hydration, syntax highlighting, and heavy CSS.

The baseline pattern: scroll container + resilient table

The safest default for technical documentation tables is boring:
wrap the table in a scroll container that owns horizontal overflow, keep the table width stable, and apply cell-level wrapping rules
to prevent single tokens from detonating the layout.

HTML structure you can ship

The wrapper is not optional. Putting overflow-x: auto directly on the table is unreliable and can break sticky headers and sizing.
Make the wrapper the scroll container; keep the table a table.

cr0x@server:~$ cat responsive-table.html
<div class="table-wrap" role="region" aria-label="API parameters table">
  <table class="doc-table">
    <thead>
      <tr>
        <th scope="col">Parameter</th>
        <th scope="col">Type</th>
        <th scope="col">Default</th>
        <th scope="col">Example</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">timeout</th>
        <td>integer</td>
        <td>30</td>
        <td><code>timeout=60</code></td>
      </tr>
    </tbody>
  </table>
</div>

CSS that behaves under pressure

The point is stability: predictable column sizing, clean overflow, and a scroll container that doesn’t hijack the page.
Also: visible affordance. If users don’t realize a table scrolls, they assume the doc is broken.

cr0x@server:~$ cat responsive-table.css
.table-wrap {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
  border-radius: 10px;
  background: Canvas;
  max-width: 100%;
}

.doc-table {
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
  min-width: 720px;
  table-layout: fixed;
}

.doc-table th,
.doc-table td {
  padding: 0.75rem 0.9rem;
  vertical-align: top;
  border-bottom: 1px solid color-mix(in srgb, CanvasText 15%, transparent);
}

.doc-table thead th {
  background: color-mix(in srgb, Canvas 92%, CanvasText 8%);
  font-weight: 650;
}

.doc-table tbody tr:hover td,
.doc-table tbody tr:hover th[scope="row"] {
  background: color-mix(in srgb, Canvas 94%, CanvasText 6%);
}

.doc-table th[scope="row"] {
  font-weight: 600;
  text-align: left;
}

.doc-table td {
  overflow: hidden;
  text-overflow: ellipsis;
}

.doc-table code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  font-size: 0.95em;
  background: color-mix(in srgb, Canvas 88%, CanvasText 12%);
  padding: 0.12em 0.35em;
  border-radius: 6px;
  white-space: normal;
  overflow-wrap: anywhere;
}

Key choices you should make deliberately:

  • min-width on the table prevents “everything squishes into unreadable soup.” On small screens, the wrapper scrolls instead.
  • table-layout: fixed reduces reflow cost and keeps column widths predictable. It also makes ellipsis work consistently.
  • Cell overflow policy is split: normal prose can wrap; code and identifiers should use overflow-wrap: anywhere and never force width.
  • Ellipsis is a tool, not a lie. Use it when a column is secondary and you offer a way to see the full value (title attribute, expandable, or copy control).

Sticky headers that don’t flicker, smear, or lie

Sticky headers are worth it for long tables—compat matrices, parameter inventories, error code lists.
But sticky headers have three classic problems: (1) they stop sticking because the wrong ancestor scrolls,
(2) they overlap content and hide focus, or (3) they misalign with columns.

Make the wrapper the scroll container, then stick inside it

position: sticky sticks relative to the nearest scrolling ancestor. If you put overflow on some parent you forgot about,
your header will stick to the wrong thing—or not at all.

cr0x@server:~$ cat sticky-header.css
.table-wrap {
  max-height: 60vh;
  overflow: auto;
}

.doc-table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
}

.doc-table thead th::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: color-mix(in srgb, CanvasText 18%, transparent);
}

.doc-table thead th {
  box-shadow: 0 2px 0 color-mix(in srgb, CanvasText 10%, transparent);
}

The z-index and a subtle shadow are not decoration. They are clarity. Without them, users can’t tell where the header ends,
especially in dark mode and especially when the header background is near the body background.

Sticky header alignment: avoid fractional widths and border chaos

Misalignment often comes from mixing border-collapse modes, nested elements with different box sizing, or content that triggers column resizing.
If you want sticky headers, you want stable widths. This is where table-layout: fixed earns its keep.

If your first column is a “Parameter name” key and must remain readable, pin it with width:

cr0x@server:~$ cat column-widths.css
.doc-table th[scope="row"] {
  width: 14rem;
}

.doc-table td:nth-child(2) {
  width: 10rem;
}

.doc-table td:nth-child(3) {
  width: 8rem;
}

Sticky first column: possible, but don’t do it casually

Sticky first columns are tempting for wide tables. They also introduce overlap and require background painting, z-index layering,
and careful handling of borders. If you need it, implement it—but treat it like a feature with a test plan, not a cute CSS trick.

cr0x@server:~$ cat sticky-first-column.css
.doc-table th[scope="row"] {
  position: sticky;
  left: 0;
  z-index: 1;
  background: Canvas;
}

.doc-table thead th:first-child {
  left: 0;
  z-index: 3;
  background: color-mix(in srgb, Canvas 92%, CanvasText 8%);
}

That last rule (top-left corner cell gets the highest z-index) is the “avoid a weird overlap artifact” tax.
Pay it upfront. You’ll pay more later if you don’t.

Readable code cells: long tokens, copyability, and wrapping

Docs tables frequently contain: commands, flags, JSON fragments, env vars, digests, regexes, and paths.
Those values are not meant to be read like a paragraph; they’re meant to be copied, compared, and scanned.
A code cell that wraps randomly can create invisible corruption. That’s how you get “it worked in staging” tickets.

Decide: wrap, scroll, or clip for code?

You have three sane choices for code-like values:

  • Wrap anywhere for identifiers where line breaks don’t change meaning (resource IDs, hashes). This preserves layout.
  • Horizontal scroll within the cell for commands and config where line breaks can mislead. This preserves copyability and semantics.
  • Clip with ellipsis when the value is secondary and there is a “copy full value” affordance.

What you should not do: allow the table to grow wider than its wrapper because a token refuses to wrap.
That’s not “responsive,” that’s “hostile.”

Pattern: scrollable code inside a cell

A simple approach: render code in a block-like element inside the cell with its own scrolling.
This avoids forcing the entire table to scroll just because one cell is long.

cr0x@server:~$ cat code-cell.css
.doc-table td .cell-code {
  display: block;
  max-width: 100%;
  overflow-x: auto;
  white-space: nowrap;
  padding: 0.35rem 0.5rem;
  border-radius: 8px;
  background: color-mix(in srgb, Canvas 88%, CanvasText 12%);
  border: 1px solid color-mix(in srgb, CanvasText 12%, transparent);
}

.doc-table td .cell-code code {
  white-space: inherit;
  background: transparent;
  padding: 0;
}

Pattern: preserve copyability and show intent

If you render line breaks in a command cell, users will copy line breaks. Then they will paste into a shell, and the shell will do shell things.
Not always the things they wanted.

Use white-space: nowrap for single-line commands; use explicit line breaks only when you’re showing a multi-line script and you want it copied as such.

Prevent “looks like a minus sign” bugs in code

Some fonts render hyphen-minus, en-dash, and em-dash differently. If your docs pipeline “smartens” punctuation,
you can end up with flags like —help that look fine but don’t work. The fix is mostly editorial (disable smart punctuation in code),
but rendering matters too: keep code in code elements so typography engines don’t get creative.

Joke #2: The only thing more dangerous than a long unbroken string is the same string wrapped at exactly the wrong character.

Accessibility and UX: don’t punish keyboard users

A scroll container is a tiny interaction model. If you don’t design it, users will discover it the hard way.
That means: discoverability, focus behavior, readable contrast, and predictable scroll.

Give the wrapper a role and label

If the table overflows, users on assistive tech should understand they’re inside a region with its own scroll.
Label it: “API parameters table,” “Error codes table,” not “table” (they already know it’s a table).

Visible focus: don’t let sticky headers hide it

Sticky headers can cover focused content when a user tabs through links inside cells.
Provide enough padding at the top of the scroll region, or ensure focused elements scroll into view with a margin.

cr0x@server:~$ cat focus.css
.table-wrap {
  scroll-padding-top: 3rem;
}

.doc-table a:focus-visible,
.doc-table button:focus-visible,
.doc-table code:focus-visible {
  outline: 2px solid color-mix(in srgb, Highlight 80%, CanvasText 20%);
  outline-offset: 2px;
}

Don’t rely on hover for meaning

Hover striping is pleasant on desktop. It does nothing on touch and little for keyboard users.
Use consistent borders, zebra striping if needed, and keep row/column headers explicit (scope attributes).

Respect reduced motion and avoid scroll-jank

Sticky headers and shadows are fine. Parallax inside a table wrapper is not.
Any scroll-linked animation becomes “my phone is hot and the table still won’t scroll.”

Performance and failure modes: when tables become a rendering incident

Tables are deceptively expensive. The browser has to compute column widths across rows, paint borders, handle sticky stacking contexts,
and reflow when fonts load or when client-side scripts “enhance” the markup.
If your docs platform does client-side hydration, a large table can cause a noticeable main-thread stall.

What tends to hurt

  • Huge tables with hundreds of rows and complex cell content (icons, nested blocks, syntax highlighting).
  • Late-loading web fonts that change metrics and force relayout, especially with position: sticky.
  • JavaScript “table plugins” that rewrite DOM, measure widths on scroll, or attach scroll listeners without throttling.
  • Heavy box shadows and filters applied to many cells.
  • Copy buttons per cell rendered as interactive controls in every row without virtualization.

What tends to help

  • Keep HTML simple. Tables already have complex layout rules; don’t nest interactive dashboards inside them.
  • Use table-layout: fixed for predictable sizing and fewer layout recalculations.
  • Prefer CSS sticky over JS. Scroll event handlers are how you make “docs page” behave like “crypto miner.”
  • Be intentional about syntax highlighting. If you highlight thousands of tokens inside tables on the client, you’re choosing pain.
  • Cap height for monster tables so the page scroll remains sane and the table scroll remains localized.

If your doc site has a performance budget (it should), tables are part of it. Nobody opens an incident because “the parameter list is slow,”
but they do open one because the release train got delayed when people couldn’t read the docs.

Three corporate mini-stories from the trenches

Incident: a wrong assumption about “mobile users don’t read tables”

An internal platform team shipped a new docs theme with a “mobile simplification.” On screens below a breakpoint, tables were converted into stacked cards.
Each row became a card; each column became a label/value pair. It looked tidy. Product signed off. Nobody tried to compare two columns side by side.

The first real users were on-call engineers pulling up a compatibility matrix during an upgrade.
They needed to compare “client version,” “server version,” and “supported cipher suites.” The card view required endless scrolling and memory.
People started screenshotting the desktop version from their laptops and sending it to phones because it was faster than the mobile view.

Then the second-order failure arrived: the card converter dropped scope="row" headers and flattened HTML.
Screen reader users got unlabeled values. The team learned about it not from an accessibility audit, but from a support escalation.

The fix was blunt and correct: preserve the table grid on mobile using a scroll container, add a visible “Scroll horizontally” affordance,
and keep the header sticky. The conversion to cards remained only for a small subset of tables explicitly marked as “non-comparative.”

Optimization that backfired: “let’s auto-size columns with JavaScript”

A docs team wanted perfect column widths: narrow “Type,” wider “Description,” and “Example” just big enough. They implemented a script that measured
the widest cell per column after render, then set explicit widths on the header cells. It worked in their test pages.

In production, the docs site used client-side navigation and deferred font loading. Every navigation triggered re-measurement.
When the font swapped in, widths changed again. Sticky headers now jittered because widths were being updated mid-scroll.
On lower-end devices, the measurement loop caused noticeable jank—exactly where you want smooth scrolling.

A particularly nasty bug showed up when code cells contained horizontally scrollable blocks.
The script measured the scroll width, not the visible width, and expanded columns to the full command length.
The wrapper stopped containing overflow. The table “escaped,” again.

The eventual fix was delightfully unsexy: remove the JavaScript sizing, adopt table-layout: fixed, set a few column widths,
and use ellipsis + copy for extremely long examples. Perfection was traded for predictability, and everyone slept better.

Boring but correct practice that saved the day: test tables with worst-case content

A different org ran docs as part of a regulated product. Their review process was not glamorous, but it was disciplined.
Every major theme change had a “table torture page”: enormous identifiers, multi-line JSON, long paths, RTL-like punctuation scenarios,
and mixed scripts. Nothing fancy. Just the worst stuff they’d ever seen in production.

During a redesign, a new CSS rule set white-space: nowrap on all td elements to “keep things tidy.”
The torture page immediately showed horizontal overflow across the entire site, not just inside table wrappers.
The regression was caught before it merged.

They also had one accessibility check: tab through the table region until you can reach the last link in the last row.
If focus disappeared behind the sticky header, the change didn’t ship.

The boring practice—keeping a worst-case page and treating it like a test suite—prevented a rollout that would have broken every API reference table.
Nobody wrote a postmortem because nothing broke. That is the best kind of reliability work.

Fast diagnosis playbook: what to check first/second/third

When a table is “broken” (overflowing, sluggish, misaligned, unreadable), you want to avoid random CSS whack-a-mole.
Run this in order. It’s designed to find the bottleneck quickly.

1) Identify the scroll owner

  • Is the wrapper the element with overflow-x/overflow?
  • Is some parent accidentally creating a scroll container?
  • Is the table wider than the wrapper because of an unbreakable token?

2) Find the widest cell (the usual suspect)

  • Look for long identifiers, base64, minified JSON, or commands without breaks.
  • Check for white-space: nowrap applied too broadly.
  • Check for min-width on cells or child elements forcing width.

3) Validate sticky behavior in the actual scroll context

  • Sticky headers need position: sticky; top: 0 on th and a defined scroll container.
  • Check stacking contexts (z-index) so header paints above body rows.
  • Confirm header background is opaque; otherwise body text bleeds through during scroll.

4) Check layout stability and render cost

  • If it’s janky, check whether JavaScript is measuring widths or reacting to scroll.
  • Check font swapping (FOIT/FOUT) and whether it causes relayout of the table.
  • Check if syntax highlighting is happening client-side for hundreds of cells.

5) Confirm keyboard/touch usability

  • Can you focus into the region and see the focus ring?
  • Does touch horizontal scroll work without triggering page navigation gestures?
  • Does the sticky header cover focused elements?

Practical tasks: commands, outputs, and decisions (12+)

These are “do the thing” tasks you can run on a doc repo or a built site. Each includes: a command, sample output, what it means,
and the decision you make from it. They assume a typical static site build, with HTML/CSS/JS artifacts.

Task 1: Find tables that aren’t wrapped (the root of most overflow bugs)

cr0x@server:~$ rg -n "

What it means: You have multiple table variants; some may be missing the wrapper.

Decision: Standardize on one wrapper component (.table-wrap) and fail builds when a bare table appears in content.

Task 2: Identify unwrapped tables via HTML parsing

cr0x@server:~$ python3 - <<'PY'
from bs4 import BeautifulSoup
import glob, sys
bad=[]
for f in glob.glob("public/**/*.html", recursive=True):
    html=open(f,encoding="utf-8").read()
    s=BeautifulSoup(html,"html.parser")
    for t in s.find_all("table"):
        p=t.parent
        if not (p and p.name=="div" and "table-wrap" in (p.get("class") or [])):
            bad.append((f, t.get("class")))
            break
print("unwrapped_files", len(set(x[0] for x in bad)))
for f,_ in bad[:10]:
    print(f)
PY
unwrapped_files 2
public/errors.html
public/compat.html

What it means: Two pages have tables without the expected scroll container.

Decision: Fix those templates/content now; otherwise any long token can break the entire page layout.

Task 3: Detect “nowrap everywhere” CSS regressions

cr0x@server:~$ rg -n "white-space:\s*nowrap" public/assets/*.css | head -n 20
public/assets/app.css:1132:.doc-table td { white-space: nowrap; }
public/assets/app.css:1180:.cell-code { white-space: nowrap; }

What it means: The first rule is dangerous; it forces every cell to refuse wrapping.

Decision: Remove nowrap from generic td; keep nowrap only for explicit code containers.

Task 4: Confirm sticky header rules exist and target the right element

cr0x@server:~$ rg -n "position:\s*sticky" public/assets/*.css
public/assets/app.css:1202:.doc-table thead th { position: sticky; top: 0; z-index: 2; }

What it means: Sticky is applied to thead th, which is the right target.

Decision: Keep it. If sticky fails at runtime, the problem is likely scroll ancestor or overflow clipping, not missing CSS.

Task 5: Find overflow clipping ancestors that break sticky

cr0x@server:~$ rg -n "overflow:\s*(hidden|clip)" public/assets/*.css | head
public/assets/theme.css:221:.content { overflow: hidden; }
public/assets/theme.css:418:.doc-container { overflow: clip; }

What it means: Overflow rules on outer containers can clip sticky headers or scrollbars.

Decision: Remove or scope overflow clipping. If you must clip, ensure the table wrapper is not inside the clipped stacking context.

Task 6: Spot the long-token landmines in content

cr0x@server:~$ rg -n "[A-Za-z0-9+/]{120,}={0,2}" content/ | head
content/auth.md:77:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....
content/trace.md:54:Zm9vYmFyYmF6cXV4cXV1eGJhc2U2NA==...

What it means: Base64/JWT-style strings exist. They will blow up tables unless you allow wrapping or local scrolling.

Decision: Ensure code cells use overflow-wrap: anywhere or inner scrolling; add copy affordances where needed.

Task 7: Check for table-layout policy (fixed vs auto)

cr0x@server:~$ rg -n "table-layout" public/assets/*.css
public/assets/app.css:1099:.doc-table { table-layout: fixed; }
public/assets/app.css:2055:.matrix { table-layout: auto; }

What it means: You have inconsistent layout modes. auto can cause expensive reflow and width surprises.

Decision: For doc tables, default to fixed. Keep auto only where you truly need intrinsic sizing (rare).

Task 8: Measure how many tables exist (scale matters for performance)

cr0x@server:~$ find public -name "*.html" -print0 | xargs -0 rg -c "

What it means: 47 tables across the built site. Not huge, but enough that global JS enhancements can hurt.

Decision: Avoid per-table heavy scripts; keep behavior CSS-first and progressive.

Task 9: Detect client-side table enhancements (JS plugins, measurement loops)

cr0x@server:~$ rg -n "(getBoundingClientRect|offsetWidth|scrollWidth).*(table|thead|th)" public/assets/*.js | head
public/assets/app.js:8812:const w = th.getBoundingClientRect().width;
public/assets/app.js:8820:table.style.width = table.scrollWidth + "px";

What it means: JavaScript is measuring header widths and possibly forcing table width to scrollWidth. That’s a red flag.

Decision: Remove or gate this logic. Replace with CSS sizing. If you keep it, run it once, not on scroll/resize loops.

Task 10: Validate that tables remain within viewport in a headless check

cr0x@server:~$ node - <<'NODE'
const fs = require("fs");
const { JSDOM } = require("jsdom");
const html = fs.readFileSync("public/api.html","utf8");
const dom = new JSDOM(html);
const tables = [...dom.window.document.querySelectorAll("table")];
console.log("tables", tables.length);
console.log("wrapped", [...dom.window.document.querySelectorAll(".table-wrap table")].length);
NODE
tables 3
wrapped 2

What it means: One table in api.html is not wrapped.

Decision: Fix the HTML generation. You want “wrapped equals tables” for doc tables unless explicitly exempted.

Task 11: Check for CLS risk from late font loading in built HTML

cr0x@server:~$ rg -n "rel=\"preload\" as=\"font\"" public/*.html | head
public/index.html:18:<link rel="preload" as="font" href="/assets/fonts/Inter.woff2" type="font/woff2" crossorigin>

What it means: Fonts are preloaded on the index page, but not necessarily on doc pages.

Decision: Ensure doc pages that contain large tables also preload critical fonts, or use a font stack with stable metrics.

Task 12: Audit for insufficient contrast in table header backgrounds

cr0x@server:~$ rg -n "thead th.*background" public/assets/*.css
public/assets/app.css:1110:.doc-table thead th { background: #f7f7f7; }
public/assets/dark.css:90:.doc-table thead th { background: #151515; }

What it means: You have explicit colors; contrast depends on text color and surrounding surfaces.

Decision: Validate contrast in both themes; prefer system colors (Canvas, CanvasText) or color-mix with guardrails.

Task 13: Catch tables inside containers that disable horizontal scroll

cr0x@server:~$ rg -n "\.table-wrap\s*\{[^}]*overflow-x:\s*(hidden|clip)" public/assets/*.css

What it means: No matches. Good: the wrapper is allowed to scroll.

Decision: Keep it that way. If a refactor adds overflow clipping here, treat it as a regression.

Task 14: Ensure table wrapper has a max-width policy

cr0x@server:~$ rg -n "\.table-wrap\s*\{[^}]*max-width" public/assets/*.css
public/assets/app.css:1072:.table-wrap { max-width: 100%; }

What it means: Wrapper constrains width to viewport/container.

Decision: Keep max-width: 100%. Without it, nested layouts can stretch unexpectedly.

Common mistakes: symptom → root cause → fix

1) Symptom: The table pushes the entire page sideways

Root cause: Unwrapped table or wrapper missing overflow-x: auto; unbreakable tokens or global nowrap.

Fix: Wrap tables in a dedicated scroll container. Add cell-level wrapping for code/IDs (overflow-wrap: anywhere), and remove global nowrap.

2) Symptom: Sticky header doesn’t stick

Root cause: Sticky element inside a non-scrolling context; ancestor has overflow: hidden/clip; or header cells aren’t the sticky elements.

Fix: Make the table wrapper the scroll container (overflow: auto) and set position: sticky on thead th. Remove overflow clipping from ancestors.

3) Symptom: Sticky header sticks, but columns don’t align

Root cause: Column widths change due to intrinsic sizing; mixed border models; JS measuring and setting widths inconsistently.

Fix: Use table-layout: fixed, set explicit widths for key columns, and stop JS width manipulation.

4) Symptom: Long commands wrap and become misleading

Root cause: Code rendered with white-space: normal and no inner scroll policy; soft wrap inserted mid-flag.

Fix: Use a .cell-code block with white-space: nowrap and overflow-x: auto, plus a copy button if appropriate.

5) Symptom: Copying from table cells produces weird spaces/newlines

Root cause: Styling inserts pseudo-elements, line breaks, or smart punctuation; nested elements add hidden text.

Fix: Keep code in <code>/<pre> with minimal decoration; avoid pseudo-content inside code; disable typographic substitution in code rendering pipeline.

6) Symptom: On iOS, horizontal scroll fights with page scroll

Root cause: Scroll chaining; wrapper not using momentum scrolling; container too small or nested inside another scroller.

Fix: Add -webkit-overflow-scrolling: touch and overscroll-behavior-x: contain. Avoid nested scroll containers where possible.

7) Symptom: Focus outline disappears behind the sticky header

Root cause: Sticky header overlaps; no scroll padding; header has higher z-index and opaque background.

Fix: Add scroll-padding-top on the wrapper and ensure focusable elements have a visible focus style.

8) Symptom: Page becomes sluggish when scrolling a long table

Root cause: JS scroll handlers; heavy shadows on many cells; syntax highlighting/hydration in large DOM.

Fix: Remove scroll handlers; simplify cell content; pre-render highlighting at build time; cap wrapper height and keep layout fixed.

9) Symptom: Header text bleeds with body text while scrolling

Root cause: Sticky header background is transparent; GPU/compositing artifacts.

Fix: Use an opaque background on thead th and a subtle shadow/border separator.

10) Symptom: The table looks fine, but users don’t realize it scrolls

Root cause: No visual affordance; scrollbars hidden by OS; wrapper blends into page background.

Fix: Add a border and slight background difference to the wrapper; optionally add a small “Scroll” hint for narrow screens.

Checklists / step-by-step plan

Step-by-step: ship a responsive table component

  1. Create a single table wrapper component that renders <div class="table-wrap" role="region" aria-label="..."> around the table.
  2. Make the wrapper the scroll owner: overflow-x: auto, max-width: 100%, and consider max-height for long tables.
  3. Use stable table sizing: table-layout: fixed, width: 100%, and a realistic min-width for readability.
  4. Implement sticky headers on thead th with top: 0, proper z-index, and an opaque background.
  5. Set cell overflow policy:
    • Prose columns: wrap normally.
    • ID/hash columns: overflow-wrap: anywhere.
    • Command/config columns: use a nested .cell-code scroller with nowrap.
  6. Keep semantics correct: th scope="col" and th scope="row" for row headers.
  7. Keyboard test: tab into the table, scroll the wrapper, and ensure focus remains visible under the sticky header.
  8. Mobile test: verify horizontal swipe scrolls the table without pulling the whole page sideways.
  9. Performance test: open the largest table page; ensure scrolling remains smooth and there’s no resize jitter after font load.
  10. Lock it in: add a CI check that rejects unwrapped tables and global nowrap rules on table cells.

Release checklist (the “don’t make me page you” edition)

  • All tables are wrapped in .table-wrap unless explicitly exempted.
  • Sticky headers work in the wrapper scroll context and remain aligned.
  • Long tokens don’t expand layout; they wrap or scroll inside cells.
  • Focus outlines are visible; sticky header does not hide focused elements.
  • Dark mode colors preserve contrast for header and code chips.
  • No JavaScript is measuring widths on scroll.
  • The “table torture page” renders correctly on a narrow viewport.

FAQ

1) Is horizontal scrolling “acceptable” in technical docs?

Yes, when the table is comparative and you preserve the grid. Make the scroll container obvious and keep headers sticky.
Forcing a card layout often destroys the primary value of the table: comparison.

2) Why not just use a JS table library?

Because most doc tables don’t need sorting, pagination, or filtering, and JS libraries tend to add weight, reflow work, and bugs.
If you truly need those features, implement them progressively and keep the base table readable without JS.

3) Should I put overflow-x: auto on the <table>?

Don’t. Put overflow on a wrapper. Tables have special layout behavior; sticky headers and width calculations behave more predictably
when the table remains a normal table element inside a scroll container.

4) What’s the best way to handle long commands in a cell?

Use an inner scrollable code block (.cell-code) with white-space: nowrap. If the command is critical, add a copy button.
Avoid wrapping commands unless you’re explicitly presenting a multi-line script.

5) Why table-layout: fixed? Doesn’t it make columns weird?

It can, if you never set any widths and cram wildly different content into columns. But for docs, predictability beats cleverness.
Set widths for key columns and let the rest share remaining space. It reduces layout thrash and keeps sticky headers aligned.

6) How do I keep a sticky header readable in dark mode?

Give it an opaque background and a subtle separator (border or shadow). Transparent headers in dark mode tend to smear visually during scroll.
Prefer system colors (Canvas/CanvasText) or carefully mixed values.

7) What about printing?

Print styles should disable sticky positioning and remove height caps so content flows. If your tables are wide, consider allowing them to
scale or break pages cleanly. Add a print stylesheet that sets .table-wrap { overflow: visible; max-height: none; }.

8) How do I show the full value when I use ellipsis?

Provide an explicit affordance: a copy button, a “Expand” toggle, or a details drawer. Don’t rely on the title attribute alone;
it’s inconsistent on mobile and not great for accessibility.

9) Do I need sticky headers for every table?

No. Use them for long tables where the header carries meaning (parameter columns, compatibility matrices). For small tables, sticky headers
can be visual noise and add complexity you don’t need.

Conclusion: what to change Monday morning

If you remember one thing: tables don’t “become responsive” by magic. You make them responsive by controlling overflow and stabilizing layout.
Wrap tables in a scroll container, make headers sticky in that scroll context, and treat code cells as a separate rendering problem.

Practical next steps:

  • Implement a standard .table-wrap component and migrate every docs table to it.
  • Adopt table-layout: fixed for doc tables and set widths for key columns.
  • Add code-cell policies: wrap-anywhere for IDs, inner scroll for commands/config.
  • Create a “table torture page” and run it on every theme change before you ship.
  • Remove JavaScript width measurement and any scroll-linked table behavior unless you can prove it’s needed.

Your docs are part of your production system. Treat the table component like a critical dependency: predictable, testable, and boring in the best way.

← Previous
Debian 13: /etc/pve looks empty after reboot — why mounts fail and how to recover
Next →
PostgreSQL vs Redis: how to stop cache stampedes from melting your DB

Leave a comment