Skip to content

a11y(2.4.11): add scroll-padding so sticky overlays don't obscure focused elements#3532

Merged
ardiewen merged 5 commits into
mainfrom
wcag/2.4.11-scroll-accommodation
Jul 10, 2026
Merged

a11y(2.4.11): add scroll-padding so sticky overlays don't obscure focused elements#3532
ardiewen merged 5 commits into
mainfrom
wcag/2.4.11-scroll-accommodation

Conversation

@rosanusi

Copy link
Copy Markdown
Contributor

Description

Fixes WCAG 2.2 SC 2.4.11 (Focus Not Obscured — Minimum) across all authenticated routes. ui-main had seven persistent sticky/fixed overlays and zero scroll accommodation — keyboard-focused elements below the fold would auto-scroll flush with the viewport top, placing them entirely behind the top-nav or other overlays.

Root cause: scroll-padding-top and scroll-padding-bottom were never applied to any scroll container in the codebase (confirmed via negative grep). The fix is structural: add scroll accommodation to the two scroll contexts where obscuring occurs.

Changes:

  • src/app.css — Adds --banner-height: 0px and --bottom-nav-height: 4rem (reset to 0px at md breakpoint) to :root so both values are always defined and mobile/desktop behave correctly without JavaScript.

  • src/lib/holocene/main-content-container.svelte — Applies scroll-padding-top: calc(var(--top-nav-height, 3rem) + var(--banner-height, 0px)) and scroll-padding-bottom: var(--bottom-nav-height, 0px) on #content-wrapper. Covers: top-nav (always present), conditional banner stacking, and mobile bottom-nav.

  • src/lib/holocene/banner/banner.svelte — Measures actual banner height via bind:clientHeight and publishes it to --banner-height on :root reactively. Resets to 0px when banner is dismissed or not shown, keeping the page-level scroll-padding-top accurate.

  • src/lib/holocene/table/paginated-table/index.svelte — Applies scroll-padding-top: var(--table-header-h, 2.25rem) and scroll-padding-bottom: {footerHeight}px on the table scroll container (nested scroll context). footerHeight is measured live via bind:clientHeight on the sticky footer div. Covers: Holocene Table sticky <thead> and paginated-table sticky footer.

Overlays addressed:

Overlay Location Fix
top-nav src/lib/components/top-nav.svelte:26 scroll-padding-top via --top-nav-height
Table sticky <thead> src/lib/holocene/table/table.svelte:74 scroll-padding-top on paginated-table container
paginated-table footer src/lib/holocene/table/paginated-table/index.svelte:65 scroll-padding-bottom tracked live
Banner conditional sticky src/lib/holocene/banner/banner.svelte:19 --banner-height measured and stacked into calc()
bottom-nav (mobile) src/lib/components/bottom-nav.svelte:138 scroll-padding-bottom via --bottom-nav-height
Drawer src/lib/holocene/drawer.svelte:70 No fix needed — contains own focus trap
Toaster src/lib/holocene/toaster.svelte:26 No fix needed — transient, auto-dismisses

Screenshots

No visual change at rest. The fix affects scroll position when focusing elements below the viewport fold via keyboard navigation.

Design

N/A — structural CSS fix, no visual design change.

Testing

  • Keyboard tab-through on a long workflow list (50+ rows) — confirm focused row is visible above top-nav
  • With a banner rendered — confirm focused element clears top-nav + banner combined height
  • Mobile viewport — confirm focused element clears bottom-nav
  • Holocene Table (any paginated list) — confirm focused cells in table body are not hidden behind sticky <thead>
  • Paginated table footer — confirm focused elements in last rows are not hidden behind sticky footer
  • Dismiss banner — confirm --banner-height resets to 0px and scroll-padding-top shrinks accordingly
  • Anchor links and programmatic scrollIntoView — confirm no regression

Checklist

  • I have performed a self-review of my own code
  • I have added tests for my changes (N/A — CSS-only; deterministic from spec)
  • I have read the contributor guidelines

Docs

No documentation changes required.

A11y-Audit-Ref: 2.4.11-scroll-accommodation-missing

…elements

Ensures keyboard-focused items are not hidden behind the top nav, sticky
banners, bottom nav, or sticky table headers/footers per SC 2.4.11.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
holocene Ready Ready Preview, Comment Jul 10, 2026 6:39pm

Request Review

@temporal-cicd

temporal-cicd Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

📊 Strict Mode: 1 error in 1 file (0.1% of 895 total)

src/lib/holocene/banner/banner.svelte (1)
  • L72:13: Type 'null' is not assignable to type '"search" | "link" | "success" | "error" | "action" | "activity" | "add-square" | "add" | "apple" | "archives" | "arrow-down" | "arrow-left" | "arrow-up" | "arrow-right" | "ascending" | ... 146 more ... | "xmark-square"'.

Generated by 🚫 dangerJS against f943fc8

@rosanusi
rosanusi marked this pull request as ready for review June 11, 2026 16:35
@rosanusi
rosanusi requested a review from a team as a code owner June 11, 2026 16:35
@ardiewen ardiewen changed the title [WCAG 2.4.11] Add scroll-padding to prevent sticky overlays from obscuring focused elements a11y(2.4.11): add scroll-padding so sticky overlays don't obscure focused elements Jul 6, 2026
@github-actions github-actions Bot added a11y Accessibility audit PR a11y:bucket-3 Bucket 3: engineer required a11y:sc-2.4.11 labels Jul 6, 2026
ardiewen and others added 2 commits July 10, 2026 13:10
…commodation-review

# Conflicts:
#	src/lib/holocene/banner/banner.svelte
… destroy

Review follow-ups on the scroll-padding fix:

- app.css: compose --scroll-inset-top/--scroll-inset-bottom per breakpoint.
  Mobile has no top-nav (top-nav is `{#if md.current}`) and the banner is
  bottom-anchored (`max-md:fixed max-md:bottom-16`), so the previous constant
  top inset over-reserved space on mobile and never covered the mobile bottom
  banner. Desktop keeps top-nav + banner on top, bottom cleared.
- main-content-container: consume the composed insets; drops the invalid
  unitless-0 calc() fallbacks.
- banner: reset --banner-height to 0px on destroy so a stale height can't
  inflate scroll-padding-top after navigating away from a page with a banner.
- paginated-table: remove stray blank line in the style attribute.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@ardiewen

Copy link
Copy Markdown
Contributor

Merged main and pushed two follow-ups:

  • Breakpoint-aware insets (app.css): the original always added top-nav + banner to scroll-padding-top, but on mobile there's no top-nav ({#if md.current}) and the banner is bottom-anchored (max-md:fixed bottom-16). Now --scroll-inset-top/-bottom are composed per breakpoint (desktop → top, mobile → bottom), so mobile no longer over-reserves the top and actually accounts for the bottom overlay. Also dropped the invalid unitless-0 calc() fallbacks.
  • Banner cleanup: reset --banner-height to 0px on destroy so a stale value can't inflate scroll-padding-top after navigating away.

banner.svelte conflicted with main's live-region change — resolved as a union (kept role/aria-atomic + the height instrumentation).

Verified with keyboard nav that the sticky-header padding does fire (a focused row parked under the header gets scrolled clear; without it the browser skips the scroll, thinking the row is already visible). Note it's an upward/Shift+Tab-only, ~1-row effect, so it's easy to miss when Tabbing downward.

The blank line before the custom property in paginated-table's style
attribute is required by stylelint's custom-property-empty-line-before
rule; my earlier cosmetic removal broke CI lint.
@ardiewen
ardiewen merged commit ddc7f8e into main Jul 10, 2026
18 checks passed
@ardiewen
ardiewen deleted the wcag/2.4.11-scroll-accommodation branch July 10, 2026 20:25
rossedfort added a commit that referenced this pull request Jul 13, 2026
Auto-generated version bump from 2.52.0 to 2.52.1

Bump type: patch

Changes included:
- [`3be0df00`](3be0df0) fix(schedules): default absent second/minute/hour to 0 when editing (#3653)
- [`508b65b7`](508b65b) fix(schedules): default emptied second/minute/hour to 0 on submit (#3655)
- [`1f0e149c`](1f0e149) fix(workers): reject $LATEST qualifier in Lambda ARN validation (#3657)
- [`b42b0776`](b42b077) a11y(2.4.3): inert-based focus containment + restore; drop trap from native dialogs (#3598)
- [`cc491a29`](cc491a2) a11y(2.4.3): keep toast live region announcing during a focus trap [DT-4252] (#3654)
- [`ddc7f8e2`](ddc7f8e) a11y(2.4.11): add scroll-padding so sticky overlays don't obscure focused elements (#3532)
- [`b7e3a472`](b7e3a47) FilterBar icon update (#3660)
- [`09318ec6`](09318ec) Add danger PR comment for api version bump (#3662)
- [`273cc4be`](273cc4b) a11y(4.1.2): add label prop to Button primitive and enforce accessible names (#3556)
- [`4507115f`](4507115) Use an editable combobox for the Cloud Run region field (DT-4232) (#3636)
- [`694a7471`](694a747) a11y(1.4.13): migrate saved-query nav tooltips to Tooltip primitive (#3607)
- [`8b384a40`](8b384a4) a11y(4.1.2): fix nested interactive elements in Copyable component (#3558)
- [`1bfd2750`](1bfd275) fix describe SANO API requests to include runId parameter (#3661)
- [`ba18b43e`](ba18b43) a11y(4.1.2): require label on CodeBlock primitive; add aria-readonly for read-only mode (#3557)
- [`6e2290f1`](6e2290f) feat(deployments): make compute provider picker configurable (#3659)
- [`4470b76a`](4470b76) fix(workers): persist WDV compute-config edits via update mask; revert $LATEST validation (#3658)
- [`17fd591b`](17fd591) fix(nexus-operations): make start standalone nexus operation timeouts optional (#3656)

Co-authored-by: rossedfort <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a11y:bucket-3 Bucket 3: engineer required a11y:sc-2.4.11 a11y Accessibility audit PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants