dcc.Tab keyboard navigation (accesibility requirements)

Due to new legislation WCAG2.1 my site needs to have keyboard navigation enabled. Which is giving me a few headaches and some issues with the dcc.Tab.

My preliminary site is organised via 5 tabs, and I have failed at making them accessible via keyboard navigation. Any assistance would be much appreciated.

1 Like

I just had an accessibility review that flagged the same thing. I haven’t spent much time looking into it yet, but this is an absolute must-have. If anyone has any suggestions, they’d be greatly appreciated. In the meantime, I’ll be contacting Plotly support.

You could try the Keyboard component in dash extensions (0.0.24). Here is a small example,

import dash
import dash_html_components as html
import json

from dash.dependencies import Output, Input
from dash_extensions import Keyboard

app = dash.Dash()
app.layout = html.Div([Keyboard(id="keyboard"), html.Div(id="output")])


@app.callback(Output("output", "children"), [Input("keyboard", "keydown")])
def keydown(event):
    return json.dumps(event)


if __name__ == '__main__':
    app.run_server()
1 Like

That’s pretty neat. I’m not sure how I would use that to help with the tab-indexing though. I have users that need to be able to press the tab key to highlight a tab and then select it by pressing the Enter key. If you run the example below, notice that the selection changes when you press the tab key, but that none of the tabs are included in the possible selections. I was toying with the source code to include tabindex=“0” for each tab, but even then, it doesn’t seem like pressing 'Enter" does anything when the tab is selected.

import dash
import dash_html_components as html
import dash_core_components as dcc

from dash.dependencies import Input, Output

app = dash.Dash(__name__)

tabs_styles = {
    'height': '44px'
}
tab_style = {
    'borderBottom': '1px solid #d6d6d6',
    'padding': '6px',
    'fontWeight': 'bold'
}

tab_selected_style = {
    'borderTop': '1px solid #d6d6d6',
    'borderBottom': '1px solid #d6d6d6',
    'backgroundColor': '#119DFF',
    'color': 'white',
    'padding': '6px'
}

app.layout = html.Div([
    dcc.Tabs(id="tabs-styled-with-inline", value='tab-1', children=[
        dcc.Tab(label='Tab 1', value='tab-1', style=tab_style, selected_style=tab_selected_style),
        dcc.Tab(label='Tab 2', value='tab-2', style=tab_style, selected_style=tab_selected_style),
        dcc.Tab(label='Tab 3', value='tab-3', style=tab_style, selected_style=tab_selected_style),
        dcc.Tab(label='Tab 4', value='tab-4', style=tab_style, selected_style=tab_selected_style),
    ], style=tabs_styles),
    html.Div(id='tabs-content-inline')
])

@app.callback(Output('tabs-content-inline', 'children'),
              [Input('tabs-styled-with-inline', 'value')])
def render_content(tab):
    if tab == 'tab-1':
        return html.Div([
            html.H3('Tab content 1'),
            html.P('Tab Index 1', tabIndex="0"),
            html.P('Tab Index 2', tabIndex="0"),
            html.P('Tab Index 3', tabIndex="0")
        ])
    elif tab == 'tab-2':
        return html.Div([
            html.H3('Tab content 2'),
            html.P('Tab Index 1', tabIndex="0"),
            html.P('Tab Index 2', tabIndex="0"),
            html.P('Tab Index 3', tabIndex="0")
        ])
    elif tab == 'tab-3':
        return html.Div([
            html.H3('Tab content 3'),
            html.P('Tab Index 1', tabIndex="0"),
            html.P('Tab Index 2', tabIndex="0"),
            html.P('Tab Index 3', tabIndex="0")
        ])
    elif tab == 'tab-4':
        return html.Div([
            html.H3('Tab content 4'),
            html.P('Tab Index 1', tabIndex="0"),
            html.P('Tab Index 2', tabIndex="0"),
            html.P('Tab Index 3', tabIndex="0")
        ])

if __name__ == '__main__':
    app.run_server(debug=True)

I finally found a solution for this by making the following changes to the dash_core_components.min.js file. The modified sections of the code are copied below, but they’ll have to be manually modified after an update. I’ll pass this along to Plotly support as well. Who knows, maybe they’ll include something similar in a future update.

Tabs (Additional code):

onKeyPress:function(){u||s(g)},tabIndex:"0",role:"tab"

Tabs Before:

return u&&(m+=" tab--disabled ".concat(b||"")),r&&(m+=" tab--selected ".concat(c||"")),y=tn(Array,t)?t[0].props.children:t,i.a.createElement("div",{"data-dash-is-loading":f&&f.is_loading||void 0,id:n,style:h,onClick:function(){u||s(g)},className:nn.a.dynamic([["1358318571",[p.background,p.border,p.border,p.border,p.primary,d,p.border,A?"":"width: calc(100% / ".concat(C,");

Tabs After:

return u&&(m+=" tab--disabled ".concat(b||"")),r&&(m+=" tab--selected ".concat(c||"")),y=tn(Array,t)?t[0].props.children:t,i.a.createElement("div",{"data-dash-is-loading":f&&f.is_loading||void 0,id:n,style:h,onClick:function(){u||s(g)},onKeyPress:function(){u||s(g)},tabIndex:"0",role:"tab",className:nn.a.dynamic([["1358318571",[p.background,p.border,p.border,p.border,p.primary,d,p.border,A?"":"width: calc(100% / ".concat(C,");

Tabs Parent (Additional code):

role:"tablist"

Tabs Parent (Before):

return i.a.createElement("div",{"data-dash-is-loading":this.props.loading_state&&this.props.loading_state.is_loading||void 0,style:this.props.parent_style,id:"".concat(this.props.id,"-parent"),className:nn.a.dynamic([["2495343579",[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]]])+" "+"".concat(s," ").concat(this.props.parent_className||"")},i.a.createElement("div",{style:this.props.style,id:this.props.id,className:nn.a.dynamic([["2495343579",[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]]])+" "+"".concat(c," ").concat(this.props.className||"")},e),i.a.createElement("div",{style:this.props.content_style,className:nn.a.dynamic([["2495343579",[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]]])+" "+"".concat(l," ").concat(this.props.content_className||"")},a||""),i.a.createElement(nn.a,{id:"2495343579",dynamic:[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]},".tab-parent.__jsx-style-dynamic-selector{display:-webkit-box;

Tabs Parent (After):

return i.a.createElement("div",{"data-dash-is-loading":this.props.loading_state&&this.props.loading_state.is_loading||void 0,style:this.props.parent_style,id:"".concat(this.props.id,"-parent"),role:"tablist",className:nn.a.dynamic([["2495343579",[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]]])+" "+"".concat(s," ").concat(this.props.parent_className||"")},i.a.createElement("div",{style:this.props.style,id:this.props.id,className:nn.a.dynamic([["2495343579",[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]]])+" "+"".concat(c," ").concat(this.props.className||"")},e),i.a.createElement("div",{style:this.props.content_style,className:nn.a.dynamic([["2495343579",[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]]])+" "+"".concat(l," ").concat(this.props.content_className||"")},a||""),i.a.createElement(nn.a,{id:"2495343579",dynamic:[this.props.mobile_breakpoint,this.props.colors.border,this.props.colors.border,this.props.colors.primary]},".tab-parent.__jsx-style-dynamic-selector{display:-webkit-box;
4 Likes

Hello, I am here for a `tabs’ accessibility requirement as well. Do you mind sharing your modified dash_core_components.min.js file? Also, I haven’t messed around with js files in dcc. If you don’t mind, how do I incorporate this into dash_core_components?

Sure thing. I’ll message you directly.

1 Like

Hello, for the tab accessibility I would need the modified dash_core_components.min.js file as well, if you don’t mind sharing it. Thanks in advance!

Hello, thank you for sharing this solution as the accessibility requirements are very important for my purposes as well!

I tried your solution, and it seems to work for using the “tab” button to navigate through the tabs, but hitting enter does nothing to actually select them. Any ideas?

In case it makes a difference, on my specific installation I had a dash_core_components.js file (rather than min.js). Not sure if that changes the exact syntax needed to update the file correctly.

Thank you!

Any update or status on this? Has anyone gotten it to work for being able to tab navigate and select? Also, any tips for how i am able to locate the dash_core_components.min.js file?

We’ve built a treemap that we’re hoping to put up on a site, but as a public sector actor if we can’t get this to work we’ll have to scrap it all. In that case does anyone know of alternative libraries that have decent accesibilty?

Thanks in advance!

Hi @drelan

The dash mantine component tabs can use the arrow keys to select tabs. Would that work for you?