How to make shiny::navlistPanel() keyboard accessible

I am trying to optimize the accessibility of my shiny app for users that use screen readers. This involves making sure the app is navigable using the keyboard (mainly the tab key). I have run into a problem with the navlistPanel() ui element as the tabs are not keyboard-focusable. I think this may be due to the elements having a tabindex of -1. How can I override this behavior and make the navigation menu keyboard accessible?

MRE:

library(shiny)

ui <- fluidPage(
  navlistPanel(
    "Example Tabs",
    tabPanel("Tab 1", "First content"),
    tabPanel("Tab 2", "Second content"),
    tabPanel("Tab 3", "Third content")
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

Usually I would have recommended using htmltools::tagQuery or tagAppendAttributes().

However, in this case tagQuery(tabPanel("Tab 2", "Second content")) can only access the div tags not the relevant a tags because they are only generated when the server is up.

We can use JS directly instead to modify the tabindex attribute:

library(shiny)
library(htmltools)

ui <- fluidPage(
  tags$script(HTML(
    "
    Shiny.initializedPromise.then(() => {
      const links = document.getElementById('navlistID').getElementsByTagName('a');
      for (const element of links) {
        element.setAttribute('tabindex', 0);
      }
    }); 
    
    "
  )),
  navlistPanel(
    "Example Tabs",
    id = "navlistID",
    tabPanel("Tab 1", "First content"),
    tabPanel("Tab 2", "Second content"),
    tabPanel("Tab 3", "Third content")
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

Relevant docs:

Thanks for your reply. That solution didn't quite work, but the strategy was very helpful. After playing around with it some, this is what worked:

library(shiny)
library(htmltools)

ui <- fluidPage(
  tags$script(HTML("
    function makeNavAccessible() {
      const navList = document.querySelector('ul[data-tabsetid]');
      if (navList) {
        const links = navList.querySelectorAll('a[role=\"tab\"]');
        links.forEach(link => link.setAttribute('tabindex', '0'));
      }
    }
    
    function setupAccessibility() {
      const navList = document.querySelector('ul[data-tabsetid]');
      if (!navList) {
        setTimeout(setupAccessibility, 100);
        return;
      }
      
      // Initial fix
      makeNavAccessible();
      
      // Fix after clicks
      navList.addEventListener('click', () => {
        setTimeout(makeNavAccessible, 50);
      });
      
      // Watch for DOM changes
      new MutationObserver(() => makeNavAccessible()).observe(navList, {
        attributes: true,
        attributeFilter: ['tabindex'],
        subtree: true
      });
    }
    
    $(document).ready(setupAccessibility);
  ")),
  
  navlistPanel(
    "Example Tabs",
    id = "navlistID",
    tabPanel("Tab 1", "First content"),
    tabPanel("Tab 2", "Second content"), 
    tabPanel("Tab 3", "Third content")
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

Would you please explain what didn't work for you? I was able to tab through the links just fine.

I could tab through the links on startup, but once I selected a different link, the previous one would revert to a tabindex of -1 and couldn't be tabbed again.

Ah okay - My tests didn't go that far :sweat_smile:

Btw. did you consider bringing this topic up as a Github issue?

Yeah I'll add it soon! It would be nice if there was more accessibility built into Shiny in general.

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.