Skip to content

feat(table-container): sticky table header for tall tables - #1585

Draft
caugner wants to merge 13 commits into
mainfrom
sticky-table-headers
Draft

caugner wants to merge 13 commits into
mainfrom
sticky-table-headers

Conversation

@caugner

@caugner caugner commented May 19, 2026 •

Copy link
Copy Markdown
Contributor

Description

Add a sticky <thead> for tables wrapped in .table-container that have at least 10 body rows. A new hooks/sticky-table-header.js:

  • clones each qualifying <thead> into a position: fixed overlay placed just below the sticky page header,
  • mirrors the table-container's horizontal scroll position via transform: translateX(...), and
  • copies each header cell's computed width so columns stay aligned.

The overlay shifts up by the cell's computed border-top-width so it overlaps the breadcrumbs bar's 1px bottom border instead of stacking with it.

Motivation

Make column headers remain visible while scrolling through long content tables, without changing how .table-container handles wide tables. Native position: sticky can't be used because .table-container sets overflow-x: auto, which makes it a scroll container on both axes and confines sticky positioning within it instead of relative to the viewport.

Additional details

  • Opt-in by row count (≥10 body rows) to avoid wasted DOM on short tables.
  • Vertical offset is read from .page-layout__header's bottom edge at runtime, so it follows the page header height.
  • Horizontal scroll within .table-container is mirrored on the clone by translating it by the offset between the table and container rects (direction-agnostic, absorbs container padding).
  • The clone is aria-hidden, its focusable descendants get tabindex="-1", and duplicated ids are stripped: links inside <th> stay clickable while the clone is shown, but are not announced or reachable via Tab twice, and fragment links keep targeting the original table. Clicks elsewhere on the clone are absorbed by the overlay.
  • The overlay copies the classes of the closest .content-section so the clone picks up the same table styles; tables outside one are skipped.
  • ResizeObserver keeps cell widths in sync when the table or container resizes; plain scroll frames only reposition the clone.
  • The overlay is layered below the mobile sidebar and hidden in print.

Example: https://fred-pr1585.review.mdn.allizom.net/en-US/docs/Web/HTML/Reference/Attributes#attribute_list

Related issues or pull requests

Fixes #933.

Clone the `<thead>` into a fixed-position overlay that appears below the
sticky page header when a `.table-container` table with at least 10 body
rows scrolls past the breadcrumbs bar. The overlay mirrors the table's
horizontal scroll position and shifts up by the cell's `border-top-width`
to overlap the breadcrumbs bar's bottom border.

Native `position: sticky` cannot be used here because `.table-container`
sets `overflow-x: auto`, which makes it a scroll container on both axes
and confines sticky positioning within it instead of relative to the
viewport.
@github-actions

github-actions Bot commented May 19, 2026 •

Copy link
Copy Markdown
Contributor

8ab3f6d was deployed to: https://fred-pr1585.review.mdn.allizom.net/

caugner and others added 12 commits September 16, 2026 15:26
The mobile left sidebar is a fixed overlay at `--z-index-sidebar-mobile`,
so a sticky table header at `--z-index-sticky-header - 1` floated above
it while the sidebar was open.
The overlay is `position: fixed`, which repeats on every printed page
when the header happens to be stuck at print time.
Use the actual offset between the table and container rects instead of
`-scrollLeft`. This absorbs any container padding or border and is
direction-agnostic, should RTL locales ever be added.
`aria-hidden` and `pointer-events: none` left cloned links and buttons
in `<th>` keyboard-focusable. `inert` covers focus, hit testing, and the
accessibility tree at once. Duplicated `id`s on cloned cells are removed
so fragment navigation keeps targeting the original table.
`syncSize` reads a rect per header cell plus a computed style, forcing
layout on every scroll frame while stuck. Widths only change when the
table or container resizes, so run it on the stick transition and on
resize signals, and only reposition the clone on plain scroll frames.
Copy the classes of the closest `.content-section` onto the overlay
instead of hardcoding `content-section`, and skip tables outside one,
since that is where the table styles the clone relies on are scoped.
Inert nodes are skipped by hit testing, so with `inert` on the overlay
clicks fell through to links underneath. Mark only the cloned table
inert; the wrapper stays hittable and absorbs the click.
Replace `inert` with `aria-hidden` plus `tabindex="-1"` on focusable
descendants. The clone stays out of the tab order and the accessibility
tree, but pointer events reach its links so header links keep working
while stuck. Clicks elsewhere on the clone are still absorbed by the
overlay.

This branch has not been deployed

No deployments
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.

Make table header sticky on lengthy tables

2 participants