Skip to content

A11Y: Improve semantics of visual tab panels that are links#3107

Closed
lvachon1 wants to merge 9 commits intomainfrom
lev/a11y/tabs
Closed

A11Y: Improve semantics of visual tab panels that are links#3107
lvachon1 wants to merge 9 commits intomainfrom
lev/a11y/tabs

Conversation

@lvachon1
Copy link
Copy Markdown
Contributor

@lvachon1 lvachon1 commented Apr 13, 2026

Scope

Asana Ticket: ♿ Improve semantics of visual tab panels that are links

Implementation

Two different implementations for two different behaviors:

For the tabs on the main page (Routes, Trip Planner, Alerts) I updated the existing javascript that currently hides the appropriate content and sets the css classes to also set the aria attributes and respect keyboard navigation.

For the tabs on the other pages that are links to new pages I created a generic keyboard event hander to enabled keyboard navigation and to update the aria-selected attribute accordingly.

Users should be able to use the arrow keys to switch which tab is focused, and then either Space or Enter to activate that tab.

Screenshots

MBTA.com index page tabs

Screen.Recording.2026-04-13.at.3.19.14.PM.mov

Alerts page mode tabs

Screen.Recording.2026-04-13.at.3.20.01.PM.mov

Schedule/Line page tabs

Screen.Recording.2026-04-13.at.3.20.48.PM.mov

Stops page tabs

Screen.Recording.2026-04-13.at.3.21.20.PM.mov

How to test

http://localhost:4001/
http://localhost:4001/alerts
http://localhost:4001/schedules/Orange/line
http://localhost:4001/stops/subway


@lvachon1 lvachon1 requested a review from a team as a code owner April 13, 2026 19:26
@lvachon1 lvachon1 requested a review from thecristen April 13, 2026 19:26
Copy link
Copy Markdown
Contributor

@joshlarson joshlarson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The homepage behavior still doesn't look quite right - if that's an easy fix, I think it's probably worth doing.

Otherwise this looks great! I always found the space-bar-scrolls-down-a-bit behavior so annoying!

Comment thread assets/js/tabbed-nav.js
Copy link
Copy Markdown
Collaborator

@thecristen thecristen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So work for two tickets got combined here, but those two tickets were separate for a reason I will call out -- the homepage tabs exhibit tab behavior (switching content views without navigating away) while all the other "tabs" in MBTA.com behave like lists of links (supporting navigation, right-click to open, loading new pages), thus they should have different semantics.

  • This PR does a great job at adding the necessary semantics and keyboard navigation to the homepage tabs, with the exception of the incomplete behavior of pressing Tab from the tablist (noted in a comment)
  • Navigation links don't need aria-selected, but setting aria-current="page" IS helpful
  • Using a <nav> is appropriate for the navigation links, so thanks for that
  • The navigation links don't need the fancy keyboard behavior expected for tabbed interfaces -- likely the default HTML behavior is fine.

Hoping this makes sense. Happy to discuss if you have any questions, this was interesting to ponder over back when I was reading NCAM's feedback and writing these tickets.

slug = slug(title)
%>
<%= PhoenixHTMLHelpers.Link.link to: "#{href}\##{slug}", data: [scroll: "false"], id: slug, class: "btn #{selected_class}" do %>
<%= PhoenixHTMLHelpers.Link.link to: "#{href}\##{slug}", data: [scroll: "false"], id: slug, aria_selected: args.selected, aria_current: (if args.selected, do: "page"), class: "btn #{selected_class} tabnav" do %>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might not be working as intended, given what I see in the generated markup on /stops/ (all three links have the same aria-current and aria-selected values)

Comment thread assets/js/tabbed-nav.js

item.addEventListener("click", callback);
item.addEventListener("keyup", e => {
item.addEventListener("keydown", e => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(blocking): I think we need one more clause here -- according to the ARIA authoring guidance on manual tabs, when pressing Tab the focus should move from the tab to the next element in the page tab sequence outside the tablist. e.g. if I'm focused on the "Trip Planner" tab, I'd expect Tab to take me to the "From" input, instead of to the next tab button.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some JS so that [tab] no longer focuses the next tab, but instead focuses the next focusable item. [shift]+[tab] does the same, but in the other direction. I'm not sure if I like it though, it seems sorta janky. Part of me feels like completely remaking these tabs so we don't have to rely on so much JS.

Comment thread assets/js/tabbed-nav.js
tab.removeAttribute("aria-current");
if (clickedTab) {
if (updateContent) {
tab.setAttribute("aria-current", "page");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tabs aren't pages, and I think we don't need aria-current on the homepage tabs at all. Excerpt from the WAI-ARIA doc (https://w3c.github.io/aria/#aria-current):

Authors SHOULD NOT use the aria-current attribute as a substitute for aria-selected in widgets where aria-selected has the same meaning. For example, in a tablist, aria-selected is used on a tab to indicate the currently-displayed tabpanel

…ter a tab is activated and [tab] is pressed.
@lvachon1
Copy link
Copy Markdown
Contributor Author

Closing this PR to break it out into two different ones. Here's the first: #3114

@lvachon1 lvachon1 closed this Apr 15, 2026
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.

3 participants