Sticky Group Headers While Scrolling

Permalink to "Sticky Group Headers While Scrolling"

In a long grouped table, the group header scrolls away after a few rows and sighted users lose track of which group they are reading. Sticky group headers fix that: the current group’s header pins below the column headers until the next group pushes it away. The accessibility risks are concrete. Sticky stacks can cover the focused row (failing SC 2.4.11), take half the screen at 400% zoom, and give sighted users context that screen reader users never receive.

This guide belongs to row grouping and aggregation.

Spec reference

Permalink to "Spec reference"
  • SC 2.4.11 Focus Not Obscured (Minimum) (AA) — the focused element must not be entirely hidden by author-created content such as sticky headers.
  • SC 2.4.12 Focus Not Obscured (Enhanced) (AAA) — no part of the focused element hidden.
  • SC 1.4.10 Reflow (AA) — sticky content must not make the page unusable at 320 px.
  • SC 1.3.1 (A) — group context available programmatically.
Sticky stack at the top of the scroll container Layers: the column header row sticks at the top; the level 1 group header sticks beneath it; the level 2 group header beneath that; scrolled rows pass under the stack, and scroll padding keeps the focused row below it. Sticky stack at the top of the scroll containerColumn headerssticky top: 0Region: Northsticky top: 40px (level 1)North › Overduesticky top: 76px (level 2)Scrolling rowsscroll-padding-top: 112px keeps focus visible
Scroll padding equals the stack height.

When to use — and when not to

Permalink to "When to use — and when not to"

Use sticky group headers for long grouped tables where groups span more than a screen. Limit stacking to two levels; three or more stacked headers consume too much vertical space, especially on small screens and at high zoom.

Do not make group headers sticky when groups are short; constant pushing and replacing is distracting. The misapplication: sticky headers without scroll padding, so keyboard users tabbing down the table land on rows hidden underneath.

Sticky without padding versus sticky with padding Comparison: without scroll padding, focused rows land behind the sticky stack; with scroll padding and zoom-aware limits, focus stays visible. Sticky without padding versus sticky with padding✗ Sticky onlyFocus hidden under headersFails 2.4.11Three levels at 400% zoomContext only visual✓ Sticky with scroll paddingFocus below the stackPassesStack collapses at high zoomContext in labels too

Annotated code example

Permalink to "Annotated code example"
.grid-scroll { overflow: auto; scroll-padding-top: var(--sticky-stack); }   /* keeps focused rows visible (SC 2.4.11) */
thead th { position: sticky; top: 0; z-index: 3; }
tr.group-l1 > th { position: sticky; top: var(--head-h); z-index: 2; }
tr.group-l2 > th { position: sticky; top: calc(var(--head-h) + var(--l1-h)); z-index: 1; }
@media (max-height: 500px) {                                                  /* zoom or landscape phone */
  tr.group-l2 > th { position: static; }                                      /* reduce the stack */
}
<tr class="group-l1"><th colspan="5" scope="colgroup" id="g-north">Region: North — 120 invoices</th></tr>
<tr class="group-l2"><th colspan="5" scope="colgroup" id="g-north-overdue">North › Overdue — 14 invoices</th></tr>
<tr><th scope="row" headers="g-north-overdue">INV-1042</th><td>1,240.00</td>…</tr>

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Action Visual Screen reader
Tab or Down to a row deep in a group Group headers pinned above Row header with group (via headers)
Next group reaches the top Old header pushed away No announcement
Focus near the top Row below stack Normal reading
400% zoom Level 2 not sticky Same
Vertical space taken by sticky stacks Bar chart of the percentage of viewport height taken by sticky stacks: one level at 100% zoom 9%, two levels at 100% zoom 15%, two levels at 400% zoom 60%, collapsed stack at 400% zoom 30%. Vertical space taken by sticky stacksOne level, 100% zoompercent of viewportTwo levels, 100% zoompercentTwo levels, 400% zoomtoo muchCollapsed stack, 400% zoomacceptable
Collapse the stack at high zoom.

Step-by-step implementation

Permalink to "Step-by-step implementation"
  1. Measure the stack. Compute the heights of the column header row and each sticky level, and set CSS custom properties so sticky top values and scroll-padding-top stay in sync.
  2. Make group header cells sticky, not rows. position: sticky works on th and td more reliably than on tr across browsers.
  3. Give the stack an opaque background. Rows scrolling underneath must not show through, or text collides and fails contrast.
  4. Collapse at constrained heights. Use a height media query to drop lower levels from the stack when vertical space is tight, as at high zoom or in landscape on phones.
  5. Connect rows to groups programmatically. Use the headers attribute (or treegrid levels) so screen reader users get the same context sticky headers give sighted users.

Integration context

Permalink to "Integration context"

Sticky column headers and first columns are covered in sticky headers and first columns. Auditing obscured focus is described in focus not obscured by sticky headers. Grouping structure is in grouping by multiple columns and accessible group header rows.

Gotchas

Permalink to "Gotchas"

scrollIntoView ignores your intent. Programmatic scrollIntoView respects scroll-padding, but custom scroll code may not; route all scrolling through the same function.

Virtualized grids. In virtualized grids the sticky group header may not be in the DOM; render a separate overlay header from data and mark it aria-hidden, since rows carry context in their names.

Collapsed groups. A collapsed group’s header should not stick; it has no rows beneath it.

Edge cases

Permalink to "Edge cases"
  • Very short groups. Two adjacent short groups make the sticky header flicker; consider disabling stickiness when groups are shorter than the viewport.
  • Printing. Sticky positioning has no meaning in print; print styles should repeat group headers instead.
  • Right-to-left layouts. Sticky positioning is vertical, so it is unaffected, but sticky first columns must flip to the right side.

Worked example

Permalink to "Worked example"

A logistics dashboard grouped shipments by depot and then by status. Sticky headers worked well with a mouse, but keyboard users tabbing through rows found the focused row hidden under a 110-pixel stack, and at 400% zoom the stack covered most of the screen. The team added scroll-padding-top from measured heights, dropped the second level from the stack below 500 pixels of height and added headers references to group header cells. An audit against 2.4.11 then passed.

Common mistakes

Permalink to "Common mistakes"
  • No scroll padding.
  • Transparent sticky backgrounds.
  • Three or more sticky levels.
  • Group context only visible, not programmatic.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Provide sticky grouping as a grid option that manages the stack height variables and scroll padding automatically; do not leave teams to hand-tune offsets.

Testing it with a screen reader

Permalink to "Testing it with a screen reader"

Sticky positioning is visual, so screen reader testing focuses on context. With NVDA, move into a data row in the middle of a group using table navigation and check that the row’s group is announced through the headers association. With VoiceOver, use the rotor to move by rows and confirm the same. Then test sighted keyboard use: tab through every row of a long group with a visible focus indicator and confirm that none is hidden under the sticky stack. Finally, combine a screen magnifier at 200% with keyboard navigation and check that the magnified viewport follows focus below the sticky headers.

FAQ

Permalink to "FAQ"
Does sticky positioning affect screen readers?

No. It is visual only; the DOM order and semantics do not change.

Why not use a separate overlay header?

You can, especially in virtualized grids; hide it from assistive technology and keep context in the rows.

How much space can sticky headers take?

There is no fixed rule, but more than about a third of the viewport hurts usability, especially at zoom.

Do sticky headers fail 2.4.11 automatically?

No. They fail only when focused content becomes entirely hidden; scroll padding prevents that.

Should the sticky header announce when it changes?

No. Announcing on scroll is noisy; programmatic context covers screen reader users.

Does this work with horizontal scrolling?

Yes, but combine with a sticky first column carefully so corners layer correctly.

Permalink to "Related"

← Back to Row Grouping & Totals in Data Tables