Skip to content

Scroll wide research-post tables inside their own container - #1472

Merged
MaxGhenis merged 3 commits into
mainfrom
fix/research-table-scroll
Oct 29, 2026
Merged

MaxGhenis merged 3 commits into
mainfrom
fix/research-table-scroll

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

WIP (draft). Not for merging during Budget week (through 30 Oct 2026) without the repo owner's OK.

Problem

Research posts render markdown tables with no horizontal scroll wrapper, so a table wider than the column widens the whole page at phone width. On 28 Oct at a 375px viewport:

Change

  • website/src/components/blog/MarkdownFormatter.tsx: the table renderer now wraps each table in a new TableScroll container with overflow-x: auto and max-width: 100%. That covers GFM tables and raw HTML <table> (rehype-raw goes through the same components map), and also notebook posts, whose markdown cells use the same formatter.
    • The container takes the table's vertical margins. Inside a scroll container they would no longer collapse with the paragraphs around them, so the spacing would grow.
    • It also takes the table's shadow, which a scroll container would clip at the edges. The other table styles stay on the <table>. When the table fits the column, the container is the same size as the table, so it looks the same.
    • When the table overflows (ResizeObserver on the container and the table), the container gets tabIndex=0, role="region" and aria-label="Scrollable table", so keyboard users can scroll it. A table that fits gets no extra tab stop.
  • website/src/app/[countryId]/research/[slug]/ArticleClient.tsx: the tablet body column gets minWidth: 0, as the desktop column already has. Otherwise a flex item's automatic minimum width would let a wide table stretch the column despite the wrapper.

Invariants

  • Every rendered <table> sits directly in exactly one [data-table-scroll] container, and there are as many containers as tables (property test over 1–4 tables × 1–8 columns × 1–5 rows).
  • The container never adds a tab stop unless its content overflows.

Tests

website/src/__tests__/components/markdown-table-scroll.test.tsx (Vitest):

  • a GFM table renders inside a container with overflow-x: auto and max-width: 100%;
  • a raw HTML table is wrapped too;
  • a table that fits gets no tab stop;
  • an overflowing table gets a focusable "Scrollable table" region;
  • the fast-check property above.

Four of the five fail against origin/main's renderer. The no-tab-stop test is a guard.

Locally on Node 26, which CI pins: bun run lint, bun run typecheck, bun run format:check and bun run test in website/ all pass (57 files, 534 tests).

Browser verification

At 375px the page is now 375px wide on both posts (it was 441px on energy bills and 527px on the briefing). Wide tables scroll inside their containers, and desktop spacing and size don't change. Measurements, the tablet check and the keyboard check are in #1472 (comment).

axiom: n/a: UI-only change

🤖 Generated with Claude Code

Markdown tables in research posts rendered with no scroll wrapper, so a
table wider than the column widened the whole page on phones (the live
energy bills post was 441px wide at a 375px viewport).

Each table now sits in a TableScroll container with overflow-x: auto and
max-width: 100%. The container carries the table's vertical margins, so
they still collapse with neighbouring paragraphs, and its shadow, which a
scroll container would otherwise clip. When the table overflows, the
container becomes a focusable, labelled region so keyboard users can
scroll it. The tablet body column gets minWidth: 0, as the desktop one
already has, so a wide table can't stretch the flex column either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
policyengine-calculator-next Ready Ready Preview Oct 29, 2026 2:24pm UTC
policyengine-website Ready Ready Preview Oct 29, 2026 2:24pm UTC

Request Review

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Browser check at e47e4f6

I checked this in Chromium (the desktop app's built-in browser) against the local website dev server (next dev). For #1469's briefing I copied its markdown and its posts.json entry into the checkout for the test and did not commit them. "Before" for the briefing means origin/main's MarkdownFormatter.tsx and ArticleClient.tsx swapped into the same checkout.

Post Viewport Page scrollWidth before after Tables that now scroll in their container
/uk/research/budget-2026-energy-bills (before: live site) 375px 441px 375px 1 of 4 (Table 3, 425px)
uk-autumn-budget-2026-briefing (#1469) 375px 527px 375px 5 of 7 (390–511px)
uk-autumn-budget-2026-briefing 800px (tablet layout) 785px (no page overflow), but the body column stretched to 571px 785px; body column stays 541px 2 of 7 (558, 571px)
uk-autumn-budget-2026-briefing 1280px (desktop) 1265px 1265px 0 of 7
  • Desktop doesn't change. Each container is the same size as its table (787px wide, same height), and every table keeps 30px of space above and below, before and after.
  • Tablet needs minWidth: 0. With only the wrapper and main's ArticleClient, the tablet body column still stretched to 571px, because a flex item's automatic minimum width counts the table's width through the scroll container. With minWidth: 0 it stays at 541px.
  • Keyboard. At 375px I focused the first scrollable table, a region named "Scrollable table" with tabindex=0. Four right-arrow presses moved it 160 of its 168px, and the page stayed at scrollX 0. The focus ring shows.
  • Console. The only error is a hydration mismatch in ShareLinks (share URLs built from window.location) and in the chart useIds. That code is unchanged here; the server renders the desktop layout and the client switches to mobile.

The independent review saw one cold run in a loaded sandbox exceed
Vitest's 5s default; normally the 25 renders take well under a second.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Independent review and merge hold

  • r1 (GPT-6.1 Sol, hard tier) at e47e4f6: APPROVE, no defects found. The reviewer re-ran lint, typecheck and the new tests, confirmed the wrapper test fails against origin/main, and checked hydration, Strict Mode double-invoke, ResizeObserver cleanup and the Tr/Td row and cell indexing. It did not cover real-browser widths; those are in the browser-check comment above.
  • Delta 961bf0a: APPROVE. In the reviewer's loaded sandbox, a cold run of the fast-check property test took 6.26s, past Vitest's 5s default. 961bf0a only gives that test an explicit 30s timeout, and the delta review checked that.
  • CI at 961bf0a: all 10 checks pass, including the visual regression tests. Mergeable.

Hold: not merging during Autumn Budget week (through 30 Oct 2026) without the repo owner's OK. I'll check the gates again on 31 Oct.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merged at head cba7262 with an admin squash merge, pinned to that head (merge commit 94831f2).

  • Go: Max approved merging this today rather than on 31 Oct (d2158), after Pavel found the live briefing 535 px wide on phones.
  • Review: an independent review approved the change at 961bf0a, both the first pass and the follow-up on the timeout commit. The only later commit, cba7262, merges main into the branch. The PR still touches just three files: MarkdownFormatter.tsx, ArticleClient.tsx and the new table-scroll test.
  • Checks: all 10 checks passed on cba7262 against current main.
  • Diff: I read the files and commits at this head before merging.
  • Why admin: the only thing blocking the merge was the required approving review.

🤖 Generated with Claude Code

This branch was successfully deployed

2 active deployments
Preview – policyengine-website — cba7262a Deployed Oct 29, 2026 by vercel[bot]
Preview – policyengine-calculator-next — cba7262a Deployed Oct 29, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant