Skip to content

modebar tools are not accessible #6645

Closed

Description

@warrickball

I'm currently working in a small team on a Django website that has an embedded Plotly Dash. We're working through accessibility issues raised by pa11y-ci and have identified a few that are arising from within Plotly.js itself.

Here's the minimal example I set up based on the "getting started" example code on the Plotly.js website:

<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Plotly.js Minimal Working Example</title>
    <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
  </head>
  <body>
    <div id="tester" style="width:600px;height:250px;"></div>
    <script>
      TESTER = document.getElementById('tester');

      Plotly.newPlot( TESTER, [{
          x: [1, 2, 3, 4, 5],
          y: [1, 2, 4, 8, 16] }]);
    </script>
  </body>
</html>

I served this using a simple Python server:

$ python3 -m http.server 8500 --bind 127.0.0.1 &

and run pa11y-ci with this configuration:

$ cat .pa11yci
{
    "defaults": {
        "runners": ["axe", "htmlcs"],
	"chromeLaunchConfig": {
            "args": ["--no-sandbox"]
        }
    },
    "urls": [
	"http://127.0.0.1:8500/plotly-mwe.html"
    ]
}

which produces a number of issues (full log collapsed below), several of which look like this:

$ npx pa11y-ci
Running Pa11y on 1 URLs:
127.0.0.1 - - [15/Jun/2023 15:40:27] "GET /plotly-mwe.html HTTP/1.1" 200 -
 > http://127.0.0.1:8500/plotly-mwe.html - 22 errors

Errors in http://127.0.0.1:8500/plotly-mwe.html:
...
 • Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(1) > a)

   <a rel="tooltip" class="modebar-btn" data-title="Download plot as a png" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a>
...
✘ 0/1 URLs passed

There's one of these for each of the tooltips, presumably because they have neither id nor name attributes.

I'm not sure what the most appopriate id or name attribute would be.

Full error log
$ npx pa11y-ci
Running Pa11y on 1 URLs:
127.0.0.1 - - [15/Jun/2023 15:40:27] "GET /plotly-mwe.html HTTP/1.1" 200 -
 > http://127.0.0.1:8500/plotly-mwe.html - 22 errors

Errors in http://127.0.0.1:8500/plotly-mwe.html:

 • Page must have means to bypass repeated blocks (https://dequeuniversity.com/rules/axe/4.7/bypass?application=axeAPI)

   (html)

   <html lang="en"><head> <title>Plotly.js Min...</html>

 • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI)

   (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(15) > g:nth-child(1) > text)

   <text text-anchor="middle" x="0" y="183" data-unformatted="1" data-math="N" transform="translate(105.75,0)" style="font-family: &quot;Open Sans&quot;, verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity:
   1; white-space: p...

 • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI)

   (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(15) > g:nth-child(2) > text)

   <text text-anchor="middle" x="0" y="183" data-unformatted="2" data-math="N" transform="translate(202.87,0)" style="font-family: &quot;Open Sans&quot;, verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity:
   1; white-space: p...
 • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI)

   (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(15) > g:nth-child(3) > text)

   <text text-anchor="middle" x="0" y="183" data-unformatted="3" data-math="N" transform="translate(300,0)" style="font-family: &quot;Open Sans&quot;, verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity: 1;
   white-space: pre;...

 • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI)

   (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(15) > g:nth-child(4) > text)

   <text text-anchor="middle" x="0" y="183" data-unformatted="4" data-math="N" transform="translate(397.12,0)" style="font-family: &quot;Open Sans&quot;, verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity:
   1; white-space: p...

 • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI)

   (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(15) > g:nth-child(5) > text)

   <text text-anchor="middle" x="0" y="183" data-unformatted="5" data-math="N" transform="translate(494.25,0)" style="font-family: &quot;Open Sans&quot;, verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68); fill-opacity:
   1; white-space: p...

 • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI)

   (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(16) > g:nth-child(1) > text)

   <text text-anchor="end" x="79" y="4.199999999999999" data-unformatted="0" data-math="N" transform="translate(0,166.45)" style="font-family: &quot;Open Sans&quot;, verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68);
   fill-opacity: 1; wh...

 • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI)

   (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(16) > g:nth-child(2) > text)

   <text text-anchor="end" x="79" y="4.199999999999999" data-unformatted="5" data-math="N" transform="translate(0,147.95)" style="font-family: &quot;Open Sans&quot;, verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68);
   fill-opacity: 1; wh...

 • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI)

   (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(16) > g:nth-child(3) > text)

   <text text-anchor="end" x="79" y="4.199999999999999" data-unformatted="10" data-math="N" transform="translate(0,129.45)" style="font-family: &quot;Open Sans&quot;, verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68);
   fill-opacity: 1; w...

 • Elements must meet minimum color contrast ratio thresholds (https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI)

   (#tester > div > div > svg:nth-child(1) > g:nth-child(5) > g > g:nth-child(16) > g:nth-child(4) > text)

   <text text-anchor="end" x="79" y="4.199999999999999" data-unformatted="15" data-math="N" transform="translate(0,110.95)" style="font-family: &quot;Open Sans&quot;, verdana, arial, sans-serif; font-size: 12px; fill: rgb(68, 68, 68);
   fill-opacity: 1; w...

 • Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(1) > a)

   <a rel="tooltip" class="modebar-btn" data-title="Download plot as a png" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a>

 • Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(2) > a:nth-child(1))

   <a rel="tooltip" class="modebar-btn active" data-title="Zoom" data-attr="dragmode" data-val="zoom" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a>

 • Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(2) > a:nth-child(2))

   <a rel="tooltip" class="modebar-btn" data-title="Pan" data-attr="dragmode" data-val="pan" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a>

 • Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(2) > a:nth-child(3))

   <a rel="tooltip" class="modebar-btn" data-title="Box Select" data-attr="dragmode" data-val="select" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a>

 • Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(2) > a:nth-child(4))

   <a rel="tooltip" class="modebar-btn" data-title="Lasso Select" data-attr="dragmode" data-val="lasso" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1031 1000" cl...</a>

 • Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(3) > a:nth-child(1))

   <a rel="tooltip" class="modebar-btn" data-title="Zoom in" data-attr="zoom" data-val="in" data-toggle="false" data-gravity="n"><svg viewBox="0 0 875 1000" cla...</a>

 • Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(3) > a:nth-child(2))

   <a rel="tooltip" class="modebar-btn" data-title="Zoom out" data-attr="zoom" data-val="out" data-toggle="false" data-gravity="n"><svg viewBox="0 0 875 1000" cla...</a>

 • Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(3) > a:nth-child(3))

   <a rel="tooltip" class="modebar-btn" data-title="Autoscale" data-attr="zoom" data-val="auto" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a>
• Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(3) > a:nth-child(4))

   <a rel="tooltip" class="modebar-btn" data-title="Reset axes" data-attr="zoom" data-val="reset" data-toggle="false" data-gravity="n"><svg viewBox="0 0 928.6 1000" c...</a>

 • Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(4) > a:nth-child(1))

   <a rel="tooltip" class="modebar-btn" data-title="Toggle Spike Lines" data-attr="_cartesianSpikesEnabled" data-val="on" data-toggle="false" data-gravity="n"><svg viewBox="0 0 1000 1000" cl...</a>

 • Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(4) > a:nth-child(2))

   <a rel="tooltip" class="modebar-btn" data-title="Show closest data on hover" data-attr="hovermode" data-val="closest" data-toggle="false" data-gravity="ne"><svg viewBox="0 0 1500 1000" cl...</a>

 • Anchor element found with no link content and no name and/or ID attribute.

   (#modebar-c74f48 > div:nth-child(4) > a:nth-child(3))

   <a rel="tooltip" class="modebar-btn active" data-title="Compare data on hover" data-attr="hovermode" data-val="x" data-toggle="false" data-gravity="ne"><svg viewBox="0 0 1125 1000" cl...</a>

✘ 0/1 URLs passed

Activity

  1. alexcjohnson commented on Jun 22, 2023

    @alexcjohnson
    Collaborator

    Thanks @warrickball - do you have a reference for how exactly id and / or name attributes improve accessibility? These anchor elements are the actual modebar buttons, rel="tooltip" looks to me like an obsolete artifact we should remove (the buttons have tooltips added by CSS based on the data-title attribute).

    The buttons are created here:

    proto.createButton = function(config) {

    based on config items in buttons.js

    Obvious name would be config.name, or id maybe config.name + '-' + this.graphInfo._fullLayout._uid

  2. warrickball commented on Jun 23, 2023

    @warrickball
    Author

    I want to first disclaim that I am absolutely not an expert on any of this. My understanding is that this error is associated with principle 4.1.2 of the Web Content Accessibility Guidelines (WCAG) standards (v2.0). Some searching online led me to this advice, which I think suggests using something like the current data-title for the title field.

    I'll see if I can make that change myself and run pa11y-ci on it. If that passes, I'm happy to open a PR. I don't know much JavaScript but I'm guessing I could use the same logical block (in the code you linked) that sets data-title to also set title.

  3. warrickball commented on Jun 23, 2023

    @warrickball
    Author

    Hmmm... Just setting the title attribute didn't work and I wasn't sure about generating a whole set of short names. An alternative recommended here suggests adding a <title> tag as the first child of the inline SVG element. I copied the short description from data-title to such an element and pa11y-ci reports no error. I'll see if I can figure out how to code this.

  4. warrickball commented on Jun 23, 2023

    @warrickball
    Author

    I think I can see where the new code would go but I think it's beyond me to implement. This looks like the code that creates the icon:

    proto.createIcon = function(thisIcon) {
    var iconHeight = isNumeric(thisIcon.height) ?
    Number(thisIcon.height) :
    thisIcon.ascent - thisIcon.descent;
    var svgNS = 'http://www.w3.org/2000/svg';
    var icon;
    if(thisIcon.path) {
    icon = document.createElementNS(svgNS, 'svg');
    icon.setAttribute('viewBox', [0, 0, thisIcon.width, iconHeight].join(' '));
    icon.setAttribute('class', 'icon');
    var path = document.createElementNS(svgNS, 'path');
    path.setAttribute('d', thisIcon.path);
    if(thisIcon.transform) {
    path.setAttribute('transform', thisIcon.transform);
    } else if(thisIcon.ascent !== undefined) {
    // Legacy icon transform calculation
    path.setAttribute('transform', 'matrix(1 0 0 -1 0 ' + thisIcon.ascent + ')');
    }
    icon.appendChild(path);
    }
    if(thisIcon.svg) {
    var svgDoc = Parser.parseFromString(thisIcon.svg, 'application/xml');
    icon = svgDoc.childNodes[0];
    }
    icon.setAttribute('height', '1em');
    icon.setAttribute('width', '1em');
    return icon;
    };

    My take would be to try to add an extra child <title>...</title> at L218, where ... is the same information that goes into data-title. But I don't see that being passed down into the icon creator and am not sure how one would do that.

    The creator that writes bundled SVG code only seems to apply to newplotlylogo (see src/fonts/ploticon.js), so I guess the solution there (and for any hardcoded SVG icons added later) is to add a title tag to the hardcoded svg tag.

  5. self-assigned this
    on Jul 5, 2024
  6. removed their assignment
    on Aug 2, 2024
  7. changed the title [-]Accessibility of tools in modebar[/-] [+]modebar tools are not accessible[/+] on Aug 9, 2024
  8. added
    bugsomething broken
    P2considered for next cycle
    on Aug 9, 2024
  9. emilykl commented on Aug 13, 2026

    @emilykl
    Contributor

    @warrickball I came across this issue while scanning older issues in the repo, and I believe it's been resolved by #7429, which (among other changes) changed the modebar buttons from <a> elements to <button> elements, as well as adding data-title and aria-label attributes.

    Running npx pa11y-ci against https://cdn.plot.ly/plotly-4.0.0-rc.0.min.js (the latest release) following your suggested configuration shows no issues related to the modebar.

    I'll close this as completed; if you think that's incorrect please let us know!

  10. warrickball commented on Aug 17, 2026

    @warrickball
    Author

    Thanks for returning to this after all this time. I also just resurrected my old example and confirm that the checks now pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2considered for next cyclebugsomething broken

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions