Skip to content

fix(a11y): align DOM order with visual order in mobile bottom-nav (WCAG 1.3.2)#3441

Merged
bilal-karim merged 1 commit into
mainfrom
a11y/1.3.2-bottom-nav-links-order
Jun 3, 2026
Merged

fix(a11y): align DOM order with visual order in mobile bottom-nav (WCAG 1.3.2)#3441
bilal-karim merged 1 commit into
mainfrom
a11y/1.3.2-bottom-nav-links-order

Conversation

@bilal-karim

@bilal-karim bilal-karim commented May 22, 2026

Copy link
Copy Markdown
Member

Summary

The mobile bottom-nav drawer used flex-col-reverse to put the most-important section closest to the user's thumb at the bottom of the screen. Visually correct, but the implementation chose to reverse via CSS rather than data:

  • Sighted users saw sections in order [C, B, A] (bottom-up importance)
  • Screen readers / keyboard tab encountered [A, B, C] (DOM order)

This is the canonical WCAG 1.3.2 Meaningful Sequence failure — visual reading order diverges from programmatic reading order.

Fix

Move the reversal from CSS to a $derived in script, then use plain flex-col:

-  const isNotLastSection = (i: number): boolean => i !== sections.length - 1;
+  const visualSections = $derived([...sections].reverse());
+  const isNotLastSection = (i: number): boolean =>
+    i !== visualSections.length - 1;
...
-  <div class="flex h-full flex-col-reverse ...">
-    {#each sections as section, i (i)}
+  <div class="flex h-full flex-col ...">
+    {#each visualSections as section, i (i)}

Visual output is identical. DOM order, keyboard tab order, and screen-reader reading order now all agree.

Audit context

  • WCAG 2.2 SC 1.3.2 Meaningful Sequence (Level A) — Moderate.
  • Issue file: audit-output/issues/1.3.2-bottom-nav-links-order.md.
  • The audit's Defect 2 (portal-tooltip aria-describedby linkage) was deferred to the 1.4.13 tooltip remediation. That dependency is already satisfied by PR fix(a11y): make Tooltip keyboard-accessible, dismissible, and hoverable #3429, which adds the aria-describedby wiring.

Test plan

  • On a mobile viewport (Chrome DevTools device mode → iPhone), open the bottom-nav drawer. Confirm the visual section order is unchanged from prod.
  • With VoiceOver iOS (or TalkBack Android) on a real device, swipe through the drawer sections. Sections should be announced in the same order as they appear visually, top to bottom.
  • Tab through the drawer with a Bluetooth keyboard on a tablet. Focus should move through links in visual order.
  • Inspect the rendered DOM — <NavSection> elements should appear in visualSections order, not the reversed-by-CSS original.

🤖 Generated with Claude Code

A11y-Audit-Ref: 1.3.2-bottom-nav-links-order

@vercel

vercel Bot commented May 22, 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 Jun 3, 2026 6:45pm

Request Review

@github-actions github-actions Bot added a11y Accessibility audit PR a11y:no-fix-doc No A11y-Audit-Ref line; audit team triage a11y:bucket-3 Bucket 3: engineer required a11y:sc-1.3.2 and removed a11y:no-fix-doc No A11y-Audit-Ref line; audit team triage labels May 28, 2026
…AG 1.3.2)

Reimplements the 1.3.2 Meaningful Sequence fix on the linksSnippet
architecture introduced by #3445 (which deleted bottom-nav-links.svelte).

The drawer used flex-col-reverse to place the primary nav group at the
bottom near the thumb, so sighted users read top-to-bottom while AT and
keyboard users encountered the reverse DOM order. Move the reversal from
CSS into the data: emit the snippet in visual top-to-bottom order
(reversed copies of each group) and switch flex-col-reverse/justify-start
to flex-col/justify-end. Pixels are unchanged; DOM, tab, and screen-reader
order now match the visual order.

Copies ([...group].reverse()) are used so the arrays passed to the
desktop SideNavigation are not mutated.

A11y-Audit-Ref: 1.3.2-bottom-nav-links-order

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

ardiewen commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

⚠️ Updated approach — description above is now out of date

This branch was rebased onto main, which surfaced a modify/delete conflict: the file this PR originally edited, bottom-nav-links.svelte, was deleted by #3445 ("Make bottom-nav accept linksSnippet instead of sections"). The sections-prop component no longer exists.

The 1.3.2 fix has been reimplemented on the new linksSnippet architecture. Net change is now 2 files / 3 lines:

bottom-nav.svelte — natural DOM order, still bottom-anchored near the thumb:

- class="flex h-full flex-col-reverse justify-start gap-6 overflow-auto px-4 py-8"
+ class="flex h-full flex-col justify-end gap-6 overflow-auto px-4 py-8"

+layout.svelte (linksSnippet) — emit items in visual top-to-bottom order so reading order matches the pixels:

- {#each linkList as link, i (i)} ... {/each}
- <hr />
- {#each linkListForSecondGroup as link, i (i)} ... {/each}
+ {#each [...linkListForSecondGroup].reverse() as link, i (i)} ... {/each}
+ <hr />
+ {#each [...linkList].reverse() as link, i (i)} ... {/each}

Why this is equivalent: flex-col-reverse + justify-startflex-col + justify-end with the data pre-reversed. The reverse-axis form both bottom-anchors and flips reading order; moving the reversal into the data keeps the identical visual while making DOM / keyboard / screen-reader order match it (the actual SC 1.3.2 requirement). Reversed copies ([...group].reverse()) are used so the arrays passed to the desktop <SideNavigation> are not mutated.

Visual output is unchanged. DOM, tab, and screen-reader order now agree with the visual order.


Note: while reimplementing, I found a separate gap — the bottom-nav linksSnippet renders NavigationItem directly, bypassing NavSection's !item.hidden filter (added in #3434), so capability-gated links (Nexus / Standalone Activities) still render in the mobile bottom nav on incapable servers. That's a different SC (4.1.2) and is being handled in its own follow-up PR rather than expanding scope here.

@bilal-karim
bilal-karim merged commit e2c95d5 into main Jun 3, 2026
18 checks passed
@bilal-karim
bilal-karim deleted the a11y/1.3.2-bottom-nav-links-order branch June 3, 2026 20:01
ardiewen added a commit that referenced this pull request Jun 4, 2026
The conflict resolution for the #3441/#3494 merge left the combined
[...group].filter(...).reverse() each-expressions unformatted; rebase
--continue does not run the lint-staged pre-commit hook. Apply prettier.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
ardiewen added a commit that referenced this pull request Jun 4, 2026
…3494)

* fix(a11y): filter hidden nav items in mobile bottom-nav (WCAG 4.1.2)

The mobile bottom-nav linksSnippet renders NavigationItem directly,
bypassing NavSection's `!item.hidden` filter. As a result, capability-
gated links (Standalone Activities on server <1.30.0, Nexus when
systemInfo.capabilities.nexus is absent) still rendered as focusable
links for keyboard and screen-reader users on incapable servers — the
desktop side-nav already filters these via NavSection.

This gap was introduced when #3445 flattened NavSection into a snippet of
raw NavigationItems; #3434 added the hidden filter to NavSection only.
Apply the same `!item.hidden` predicate to both bottom-nav groups.

A11y-Audit-Ref: 4.1.2-nav-hidden-flag-ignored

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

* test(a11y): cover bottom-nav capability gating (WCAG 4.1.2)

Add a mobile integration spec verifying the bottom-nav drawer hides
capability-gated links from keyboard/AT users:
- Nexus hidden when systemInfo.capabilities.nexus is false, shown when true
- Standalone Activities hidden on server < 1.30.0, shown on >= 1.30.0

Uses the existing mockClusterApi / mockSystemInfoApi route mocks to drive
the serverVersion and capabilities the gates depend on. Verified the two
"hides" cases fail without the filter fix (genuine regression guard).

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

* style: apply prettier formatting to merged linksSnippet

The conflict resolution for the #3441/#3494 merge left the combined
[...group].filter(...).reverse() each-expressions unformatted; rebase
--continue does not run the lint-staged pre-commit hook. Apply prettier.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
rossedfort added a commit that referenced this pull request Jun 4, 2026
Auto-generated version bump from 2.50.0 to 2.51.0

Bump type: minor

Changes included:
- [`495e27f7`](495e27f) Use pageSize instead of maximumPageSize (#3422)
- [`42c58c45`](42c58c4) fix: use rem-based width for expanded side-nav (#3426)
- [`ce928ed5`](ce928ed) fix(a11y): meet 4.5:1 contrast for text-subtle and text-warning (WCAG 1.4.3) (#3437)
- [`e8ed8a49`](e8ed8a4) fix(a11y): consistent high-contrast focus rings across all Button variants (WCAG 1.4.11) (#3438)
- [`03918fb5`](03918fb) fix(a11y): prevent horizontal scroll on login page at 320px (WCAG 1.4.10) (#3440)
- [`a11555fb`](a11555f) fix(a11y): use rem-based font-size and unitless line-height in markdown reset (#3430)
- [`057ff323`](057ff32) fix: use empty alt on decorative SDK logo image (#3424)
- [`e435ffd7`](e435ffd) Standalone Activity details page UI updates (#3427)
- [`e09028f3`](e09028f) Make bottom-nav accept linksSnippet instead of sections (#3445)
- [`a22a1e21`](a22a1e2) Add tooltip for SDK version (#3462)
- [`c3b7820e`](c3b7820) [DT-4039] Workflow query builder doesn't work with numeric search attributes (#3435)
- [`5e53881f`](5e53881) Make Common Errors dismissable (#3471)
- [`2665e2f1`](2665e2f) [DT-4048] Add accessibility PR triage and notification helpers (#3465)
- [`8083cf4e`](8083cf4) feat(DT-4017): Schedules List UI Update (#3467)
- [`b5de30e3`](b5de30e) fix(markdown): allow nested lists to render as block (DT-4047) (#3463)
- [`8a6fd2de`](8a6fd2d) Add a prop to hide select/deselect controls (#3474)
- [`33ec1b3e`](33ec1b3) DT-4051: pre-populate input when starting standalone activity like this one (#3469)
- [`0d64fc20`](0d64fc2) fix(a11y): reveal Copyable's CopyButton on focus-within (WCAG 2.1.1) (#3452)
- [`321651c0`](321651c) fix: accept autocomplete prop on NumberInput, ChipInput, and Combobox (#3425)
- [`6567c222`](6567c22) fix: cap ZoomSvg container height to viewport (#3428)
- [`afd3a8ed`](afd3a8e) fix(a11y): accommodate text-spacing overrides on Badge and Chip (WCAG 1.4.12) (#3433)
- [`d264b876`](d264b87) fix(a11y): non-color signal for Label required indicator (WCAG 1.4.1) (#3439)
- [`73c61ea3`](73c61ea) fix(a11y): tabindex on <main> so the skip link moves focus reliably (WCAG 2.4.1) (#3451)
- [`34e582a8`](34e582a) fix(a11y): info-and-relationships compliance (WCAG 1.3.1) (#3432)
- [`f7be7b6c`](f7be7b6) fix(query): quote ExecutionDuration Go duration strings in visibility SQL (#3482)
- [`9f0c2631`](9f0c263) Passthrough goto params to link component (#3483)
- [`2f8c037b`](2f8c037) feat: add centerButton, menuButton, and linksContent snippets to BottomNavigation (#3485)
- [`7f258bac`](7f258ba) fix: add "for" to validate connection modal title (#3488)
- [`350397ab`](350397a) feat(DT-4069): Update modal backdrop to 50% opaque (#3486)
- [`f01baa49`](f01baa4) refactor: Input & DatePicker - Svelte 5 & afterLabel snippet (#3479)
- [`587c892f`](587c892) Fix decoded object payload summaries (#3491)
- [`2b85ef06`](2b85ef0) fix(DT-4044): only use browser codec endpoint when override option is selected (#3490)
- [`468893ba`](468893b) fix(a11y): improve 1.1.1 non-text content compliance (#3431)
- [`3e8c996d`](3e8c996) fix(a11y): make Tooltip keyboard-accessible, dismissible, and hoverable (#3429)
- [`47552538`](4755253) fix(a11y): name-role-value compliance, partial (WCAG 4.1.2) (#3434)
- [`e2c95d54`](e2c95d5) fix(a11y): align DOM order with visual order in mobile bottom-nav (WCAG 1.3.2) (#3441)
- [`634b08e1`](634b08e) a11y(1.4.11): focus rings — lighten dark-mode --color-border-focus-info to indigo.400 so ring-primary/70 meets 3:1 against surface-primary (#3478)
- [`039a555a`](039a555) a11y(1.4.11): Checkbox — add ring-offset so the checked-state focus ring contrasts against the indigo checked background (#3477)
- [`01161e2e`](01161e2) a11y(1.4.10): activity-options drawer — make width responsive so it reflows at 320 px (#3476)
- [`99f0a0a4`](99f0a0a) fix(a11y): filter hidden nav items in mobile bottom-nav (WCAG 4.1.2) (#3494)
- [`d12ebacc`](d12ebac) fix(a11y): add focusin/focusout to saved-query nav hover tooltip (WCAG 2.1.1) (#3453)
- [`c7206af9`](c7206af) fix(a11y): render a warning icon for Chip warning intent (WCAG 1.4.1) (#3450)
- [`b64ed713`](b64ed71) fix(a11y): pair Toast variant background with a severity icon (WCAG 1.4.1) (#3449)
- [`8e2ef708`](8e2ef70) upgrade temporal api version to latest (v1.62.13) (#3502)

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-1.3.2 a11y Accessibility audit PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants