title: How can I make my accordion accessible by keyboard + screen readers?
body: <p>I've been trying to make my current expandable accordions meet Level AA of the W3C Web Content Accessibility Guidelines by being both keyboard accessible and screen reader accessible. </p>
  
  <p>I am not super familiar with JavaScript/jQuery so I have been doing a lot of guess-and-check so far.</p>
  
  <p><strong>I have accomplished the following:</strong></p>
  
  <ol>
  <li>A tab-index order by using the Tab key</li>
  <li>The ability to move back and forth using the up/down or left/right keyboard keys</li>
  <li>The ability to expand/collapse an accordion by using either Enter or Spacebar</li>
  </ol>
  
  <p>But apparently <strong>I'm missing the following:</strong></p>
  
  <ol>
  <li>Cannot navigate backward using "Shift+Tab". </li>
  <li>Unable to collapse the expanded toggle as focus moves incorrectly and using shift+tab will not bring focus back to the expanded toggle.</li>
  <li>Focus does not move to the links present under the toggles.</li>
  <li>Tabs grouping is not present, the screen reader does not read tab 1 of 3, tab 2 of 3, etc.</li>
  </ol>
  
  <p>Here is the CodePen I have been using: <a href="https://codepen.io/kwhytock/pen/Ozzopr" rel="nofollow noreferrer">https://codepen.io/kwhytock/pen/Ozzopr</a>
  I included all the jQuery UI code, but the Accordion-focused code starts at line 2516.</p>
  
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>$(function() {
    $("#accordion:nth-child(1n)").accordion({
      collapsible: true
    });
    $("#accordion:nth-child(1n)").accordion({
      active: false
    });
  });
  
    var widgetsAccordion = $.widget("ui.accordion", {
      version: "1.12.1",
      options: {
        active: 0,
        animate: {},
        classes: {
          "ui-accordion-header": "ui-corner-top",
          "ui-accordion-header-collapsed": "ui-corner-all",
          "ui-accordion-content": "ui-corner-bottom"
        },
        collapsible: false,
        event: "click",
        header: ".accordionTitle",
        heightStyle: "auto",
  
        // Callbacks
        activate: null,
        beforeActivate: null
      },
  
      hideProps: {
        borderTopWidth: "hide",
        borderBottomWidth: "hide",
        paddingTop: "hide",
        paddingBottom: "hide",
        height: "hide"
      },
  
      showProps: {
        borderTopWidth: "show",
        borderBottomWidth: "show",
        paddingTop: "show",
        paddingBottom: "show",
        height: "show"
      },
  
      _create: function() {
        var options = this.options;
  
        this.prevShow = this.prevHide = $();
        this._addClass("ui-accordion", "ui-widget ui-helper-reset");
        this.element.attr("role", "tablist");
  
        // Don't allow collapsible: false and active: false / null
        if (!options.collapsible &amp;&amp; (options.active === false || options.active == null)) {
          options.active = 0;
        }
  
        this._processPanels();
  
        // handle negative values
        if (options.active &lt; 0) {
          options.active += this.headers.length;
        }
        this._refresh();
      },
  
      _getCreateEventData: function() {
        return {
          header: this.active,
          panel: !this.active.length ? $() : this.active.next()
        };
      },
  
      _createIcons: function() {
        var icon, children,
          icons = this.options.icons;
  
        if (icons) {
          icon = $("&lt;span&gt;");
          this._addClass(icon, "ui-accordion-header-icon", "ui-icon " + icons.header);
          icon.prependTo(this.headers);
          children = this.active.children(".ui-accordion-header-icon");
          this._removeClass(children, icons.header)
            ._addClass(children, null, icons.activeHeader)
            ._addClass(this.headers, "ui-accordion-icons");
        }
      },
  
      _destroyIcons: function() {
        this._removeClass(this.headers, "ui-accordion-icons");
        this.headers.children(".ui-accordion-header-icon").remove();
      },
  
      _destroy: function() {
        var contents;
  
        // Clean up main element
        this.element.removeAttr("role");
  
        // Clean up headers
        this.headers
          .removeAttr("role aria-expanded aria-selected aria-controls tabIndex")
          .removeUniqueId();
  
        this._destroyIcons();
  
        // Clean up content panels
        contents = this.headers.next()
          .css("display", "")
          .removeAttr("role aria-hidden aria-labelledby")
          .removeUniqueId();
  
        if (this.options.heightStyle !== "content") {
          contents.css("height", "");
        }
      },
  
      _setOption: function(key, value) {
        if (key === "active") {
  
          // _activate() will handle invalid values and update this.options
          this._activate(value);
          return;
        }
  
        if (key === "event") {
          if (this.options.event) {
            this._off(this.headers, this.options.event);
          }
          this._setupEvents(value);
        }
  
        this._super(key, value);
  
        // Setting collapsible: false while collapsed; open first panel
        if (key === "collapsible" &amp;&amp; !value &amp;&amp; this.options.active === false) {
          this._activate(0);
        }
  
        if (key === "icons") {
          this._destroyIcons();
          if (value) {
            this._createIcons();
          }
        }
      },
  
      _setOptionDisabled: function(value) {
        this._super(value);
  
        this.element.attr("aria-disabled", value);
  
        // Support: IE8 Only
        // #5332 / #6059 - opacity doesn't cascade to positioned elements in IE
        // so we need to add the disabled class to the headers and panels
        this._toggleClass(null, "ui-state-disabled", !!value);
        this._toggleClass(this.headers.add(this.headers.next()), null, "ui-state-disabled", !!value);
      },
  
      _keydown: function(event) {
        if (event.altKey || event.ctrlKey) {
          return;
        }
  
        var keyCode = $.ui.keyCode,
          length = this.headers.length,
          currentIndex = this.headers.index(event.target),
          toFocus = true;
  
        switch (event.keyCode) {
          case keyCode.RIGHT:
          case keyCode.TAB:
            if (event.shiftKey &amp;&amp; event.keyCode == 9) {
              //shift was down when tab was pressed
            }
            toFocus = this.headers[(currentIndex - 1) % length];
          case keyCode.DOWN:
            toFocus = this.headers[(currentIndex + 1)];
            break;
          case keyCode.LEFT:
          case keyCode.UP:
            toFocus = this.headers[(currentIndex - 1 + length) % length];
            break;
          case keyCode.SPACE:
          case keyCode.ENTER:
            this._eventHandler(event);
            break;
          case keyCode.HOME:
            toFocus = this.headers[0];
            break;
          case keyCode.END:
            toFocus = this.headers[length - 1];
            break;
        }
  
        if (toFocus) {
          $(event.target).attr("tabIndex", -1);
          $(toFocus).attr("tabIndex", 0);
          $(toFocus).trigger("focus");
          event.preventDefault();
        }
      },
  
      _panelKeyDown: function(event) {
        if (event.keyCode === $.ui.keyCode.UP &amp;&amp; event.ctrlKey) {
          $(event.currentTarget).prev().trigger("focus");
        }
      },
  
      refresh: function() {
        var options = this.options;
        this._processPanels();
  
        // Was collapsed or no panel
        if ((options.active === false &amp;&amp; options.collapsible === true) ||
          !this.headers.length) {
          options.active = false;
          this.active = $();
  
          // active false only when collapsible is true
        } else if (options.active === false) {
          this._activate(0);
  
          // was active, but active panel is gone
        } else if (this.active.length &amp;&amp; !$.contains(this.element[0], this.active[0])) {
  
          // all remaining panel are disabled
          if (this.headers.length === this.headers.find(".ui-state-disabled").length) {
            options.active = false;
            this.active = $();
  
            // activate previous panel
          } else {
            this._activate(Math.max(0, options.active - 1));
          }
  
          // was active, active panel still exists
        } else {
  
          // make sure active index is correct
          options.active = this.headers.index(this.active);
        }
  
        this._destroyIcons();
  
        this._refresh();
      },
  
      _processPanels: function() {
        var prevHeaders = this.headers,
          prevPanels = this.panels;
  
        this.headers = this.element.find(this.options.header);
        this._addClass(this.headers, "ui-accordion-header ui-accordion-header-collapsed",
          "ui-state-default");
  
        this.panels = this.headers.next().filter(":not(.ui-accordion-content-active)").hide();
        this._addClass(this.panels, "ui-accordion-content", "ui-helper-reset ui-widget-content");
  
        // Avoid memory leaks (#10056)
        if (prevPanels) {
          this._off(prevHeaders.not(this.headers));
          this._off(prevPanels.not(this.panels));
        }
      },
  
      _refresh: function() {
        var maxHeight,
          options = this.options,
          heightStyle = options.heightStyle,
          parent = this.element.parent();
  
        this.active = this._findActive(options.active);
        this._addClass(this.active, "ui-accordion-header-active", "ui-state-active")
          ._removeClass(this.active, "ui-accordion-header-collapsed");
        this._addClass(this.active.next(), "ui-accordion-content-active");
        this.active.next().show();
  
        this.headers
          .attr("role", "heading")
          .attr("type", "button")
          .each(function() {
            var header = $(this),
              headerId = header.uniqueId().attr("id"),
              panel = header.next(),
              panelId = panel.uniqueId().attr("id");
            header.attr("aria-controls", panelId);
            panel.attr("aria-labelledby", headerId);
          })
          .next()
          .attr("role", "region");
  
        this.headers
          .not(this.active)
          .attr({
            "aria-selected": "false",
            "aria-expanded": "false",
            tabIndex: -1
          })
          .next()
          .attr({
            "aria-hidden": "true"
          })
          .hide();
  
        // Make sure at least one header is in the tab order
        if (!this.active.length) {
          this.headers.eq(0).attr("tabIndex", 0);
        } else {
          this.active.attr({
              "aria-selected": "true",
              "aria-expanded": "true",
              tabIndex: 0
            })
            .next()
            .attr({
              "aria-hidden": "false"
            });
        }
  
        this._createIcons();
  
        this._setupEvents(options.event);
  
        if (heightStyle === "fill") {
          maxHeight = parent.height();
          this.element.siblings(":visible").each(function() {
            var elem = $(this),
              position = elem.css("position");
  
            if (position === "absolute" || position === "fixed") {
              return;
            }
            maxHeight -= elem.outerHeight(true);
          });
  
          this.headers.each(function() {
            maxHeight -= $(this).outerHeight(true);
          });
  
          this.headers.next()
            .each(function() {
              $(this).height(Math.max(0, maxHeight -
                $(this).innerHeight() + $(this).height()));
            })
            .css("overflow", "auto");
        } else if (heightStyle === "auto") {
          maxHeight = 0;
          this.headers.next()
            .each(function() {
              var isVisible = $(this).is(":visible");
              if (!isVisible) {
                $(this).show();
              }
              maxHeight = Math.max(maxHeight, $(this).css("height", "").height());
              if (!isVisible) {
                $(this).hide();
              }
            })
            .height(maxHeight);
        }
      },
  
      _activate: function(index) {
        var active = this._findActive(index)[0];
  
        // Trying to activate the already active panel
        if (active === this.active[0]) {
          return;
        }
  
        // Trying to collapse, simulate a click on the currently active header
        active = active || this.active[0];
  
        this._eventHandler({
          target: active,
          currentTarget: active,
          preventDefault: $.noop
        });
      },
  
      _findActive: function(selector) {
        return typeof selector === "number" ? this.headers.eq(selector) : $();
      },
  
      _setupEvents: function(event) {
        var events = {
          keydown: "_keydown"
        };
        if (event) {
          $.each(event.split(" "), function(index, eventName) {
            events[eventName] = "_eventHandler";
          });
        }
  
        this._off(this.headers.add(this.headers.next()));
        this._on(this.headers, events);
        this._on(this.headers.next(), {
          keydown: "_panelKeyDown"
        });
        this._hoverable(this.headers);
        this._focusable(this.headers);
      },
  
      _eventHandler: function(event) {
        var activeChildren, clickedChildren,
          options = this.options,
          active = this.active,
          clicked = $(event.currentTarget),
          clickedIsActive = clicked[0] === active[0],
          collapsing = clickedIsActive &amp;&amp; options.collapsible,
          toShow = collapsing ? $() : clicked.next(),
          toHide = active.next(),
          eventData = {
            oldHeader: active,
            oldPanel: toHide,
            newHeader: collapsing ? $() : clicked,
            newPanel: toShow
          };
  
        event.preventDefault();
  
        if (
  
          // click on active header, but not collapsible
          (clickedIsActive &amp;&amp; !options.collapsible) ||
  
          // allow canceling activation
          (this._trigger("beforeActivate", event, eventData) === false)) {
          return;
        }
  
        options.active = collapsing ? false : this.headers.index(clicked);
  
        // When the call to ._toggle() comes after the class changes
        // it causes a very odd bug in IE 8 (see #6720)
        this.active = clickedIsActive ? $() : clicked;
        this._toggle(eventData);
  
        // Switch classes
        // corner classes on the previously active header stay after the animation
        this._removeClass(active, "ui-accordion-header-active", "ui-state-active");
        if (options.icons) {
          activeChildren = active.children(".ui-accordion-header-icon");
          this._removeClass(activeChildren, null, options.icons.activeHeader)
            ._addClass(activeChildren, null, options.icons.header);
        }
  
        if (!clickedIsActive) {
          this._removeClass(clicked, "ui-accordion-header-collapsed")
            ._addClass(clicked, "ui-accordion-header-active", "ui-state-active");
          if (options.icons) {
            clickedChildren = clicked.children(".ui-accordion-header-icon");
            this._removeClass(clickedChildren, null, options.icons.header)
              ._addClass(clickedChildren, null, options.icons.activeHeader);
          }
  
          this._addClass(clicked.next(), "ui-accordion-content-active");
        }
      },
  
      _toggle: function(data) {
        var toShow = data.newPanel,
          toHide = this.prevShow.length ? this.prevShow : data.oldPanel;
  
        // Handle activating a panel during the animation for another activation
        this.prevShow.add(this.prevHide).stop(true, true);
        this.prevShow = toShow;
        this.prevHide = toHide;
  
        if (this.options.animate) {
          this._animate(toShow, toHide, data);
        } else {
          toHide.hide();
          toShow.show();
          this._toggleComplete(data);
        }
  
        toHide.attr({
          "aria-hidden": "true"
        });
        toHide.prev().attr({
          "aria-selected": "false",
          "aria-expanded": "false"
        });
  
        // if we're switching panels, remove the old header from the tab order
        // if we're opening from collapsed state, remove the previous header from the tab order
        // if we're collapsing, then keep the collapsing header in the tab order
        if (toShow.length &amp;&amp; toHide.length) {
          toHide.prev().attr({
            "tabIndex": -1,
            "aria-expanded": "false"
          });
        } else if (toShow.length) {
          this.headers.filter(function() {
              return parseInt($(this).attr("tabIndex"), 10) === 0;
            })
            .attr("tabIndex", -1);
        }
  
        toShow
          .attr("aria-hidden", "false")
          .prev()
          .attr({
            "aria-selected": "true",
            "aria-expanded": "true",
            tabIndex: 0
          });
      },
  
      _animate: function(toShow, toHide, data) {
        var total, easing, duration,
          that = this,
          adjust = 0,
          boxSizing = toShow.css("box-sizing"),
          down = toShow.length &amp;&amp;
          (!toHide.length || (toShow.index() &lt; toHide.index())),
          animate = this.options.animate || {},
          options = down &amp;&amp; animate.down || animate,
          complete = function() {
            that._toggleComplete(data);
          };
  
        if (typeof options === "number") {
          duration = options;
        }
        if (typeof options === "string") {
          easing = options;
        }
  
        // fall back from options to animation in case of partial down settings
        easing = easing || options.easing || animate.easing;
        duration = duration || options.duration || animate.duration;
  
        if (!toHide.length) {
          return toShow.animate(this.showProps, duration, easing, complete);
        }
        if (!toShow.length) {
          return toHide.animate(this.hideProps, duration, easing, complete);
        }
  
        total = toShow.show().outerHeight();
        toHide.animate(this.hideProps, {
          duration: duration,
          easing: easing,
          step: function(now, fx) {
            fx.now = Math.round(now);
          }
        });
        toShow
          .hide()
          .animate(this.showProps, {
            duration: duration,
            easing: easing,
            complete: complete,
            step: function(now, fx) {
              fx.now = Math.round(now);
              if (fx.prop !== "height") {
                if (boxSizing === "content-box") {
                  adjust += fx.now;
                }
              } else if (that.options.heightStyle !== "content") {
                fx.now = Math.round(total - toHide.outerHeight() - adjust);
                adjust = 0;
              }
            }
          });
      },
  
      _toggleComplete: function(data) {
        var toHide = data.oldPanel,
          prev = toHide.prev();
  
        this._removeClass(toHide, "ui-accordion-content-active");
        this._removeClass(prev, "ui-accordion-header-active")
          ._addClass(prev, "ui-accordion-header-collapsed");
  
        // Work around for rendering bug in IE (#5421)
        if (toHide.length) {
          toHide.parent()[0].className = toHide.parent()[0].className;
        }
        this._trigger("activate", null, data);
      }
    });
  
    var safeActiveElement = $.ui.safeActiveElement = function(document) {
      var activeElement;
  
      // Support: IE 9 only
      // IE9 throws an "Unspecified error" accessing document.activeElement from an &lt;iframe&gt;
      try {
        activeElement = document.activeElement;
      } catch (error) {
        activeElement = document.body;
      }
  
      // Support: IE 9 - 11 only
      // IE may return null instead of an element
      // Interestingly, this only seems to occur when NOT in an iframe
      if (!activeElement) {
        activeElement = document.body;
      }
  
      // Support: IE 11 only
      // IE11 returns a seemingly empty object in some cases when accessing
      // document.activeElement from an &lt;iframe&gt;
      if (!activeElement.nodeName) {
        activeElement = document.body;
      }
  
      return activeElement;
    };</code></pre>
  <pre class="snippet-code-css lang-css prettyprint-override"><code>.accordionTitle {
    border: 1px solid #ccc;
    margin: 5px 0 0 0;
    font-weight: 200 !important;
    font-size: 1.15em;
    background-color: #F8F8F8;
    padding: 1em 0.5em;
    text-decoration: none;
    color: #000;
    -webkit-transition: background-color 0.5s ease-in-out;
    transition: background-color 0.5s ease-in-out;
  }
  
  .accordionTitle:before {
    content: "";
    font-size: 1.5em;
    border-left: 6px solid transparent;
    border-right: 6px solid transparent;
    border-top: 6px solid;
    float: left;
    margin: 0.475em;
    margin-right: 0.55em;
    -webkit-transition: -webkit-transform 0.3s ease-in-out;
    transition: -webkit-transform 0.3s ease-in-out;
    transition: transform 0.3s ease-in-out;
    transition: transform 0.3s ease-in-out, -webkit-transform 0.3s ease-in-out;
    -webkit-transform: rotate(-90deg);
    transform: rotate(-90deg);
  }
  
  .accordionTitle[aria-selected="true"]:before {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  
  .accordionTitle:focus,
  .accordionTitle:hover {
    background-color: #dadada;
  }
  
  .ui-accordion-content {
    height: auto !important;
    overflow: hidden;
    padding: 1.5em 1.5em;
    border: 1px solid #ccc;
  }
  
  [aria-pressed=true],
  [aria-expanded=true] {
    background-color: #f9f9f9;
  }</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;script src="https://code.jquery.com/jquery-1.12.4.js"&gt;&lt;/script&gt;
  &lt;script src="http://sh101ftp.net/imgload/wordpress/jquery-ui.js"&gt;&lt;/script&gt;
  &lt;script src="http://sh101ftp.net/imgload/wordpress/NewCustomCodeJS.js"&gt;&lt;/script&gt;
  &lt;h2 id="question1" class="question"&gt;&lt;span class="dropcap dropcap3" style="color: #127eb6;"&gt;1&lt;/span&gt; &lt;span style="color: #404040;"&gt;What might help you make physical activity an ongoing thing?&lt;/span&gt;&lt;/h2&gt;
  &lt;div id="accordion" role="presentation"&gt;
  &lt;h3 class="accordionTitle"&gt;&lt;strong&gt;A.&lt;/strong&gt; Option A&lt;/h3&gt;
   &lt;div&gt;
   &lt;p&gt;This plan is practical, social, and could work well for both of you. Some disabilities an&lt;/span&gt;d other pre-existing conditions have implications for working out. Your friend knows her own body and can seek medical clearance if needed. This is her call.&lt;/p&gt;
   &lt;p&gt;&lt;u&gt;&lt;a href="http://www.prochange.com/college-health" target="_blank" rel="noopener noreferrer"&gt;liveWell program (Pro-Change Behavior Systems, Inc.)&lt;/a&gt;&lt;/u&gt;&lt;/p&gt;
   &lt;/div&gt;
  
   &lt;h3 class="accordionTitle"&gt;&lt;strong&gt;B.&lt;/strong&gt; Option B&lt;/h3&gt;
   &lt;div&gt;
   &lt;p&gt;Self-consciousness can be a barrier to working out, yes. Candy hasn’t said that’s a problem for her, though. Many people with disabilities are marginalized and excluded. We all do better when we’re socially integrated into our communities. For example, people with robust social networks (supportive friends and family) experience lower rates of chronic disease and longer lives, and more job opportunities, according to a 2011 report from the National Research Council.&lt;/p&gt;
   &lt;p&gt;&lt;u&gt;&lt;a href="http://november-project.com/" target="_blank" rel="noopener noreferrer"&gt;November Project&lt;/a&gt;&lt;/u&gt;&lt;/p&gt;
   &lt;p&gt;&lt;u&gt;&lt;a href="https://www.meetup.com/" target="_blank" rel="noopener noreferrer"&gt;Meetup&lt;/a&gt;&lt;/u&gt;&lt;/p&gt;
   &lt;/div&gt;
   &lt;h3 class="accordionTitle"&gt;&lt;strong&gt;C.&lt;/strong&gt; Option C&lt;/h3&gt;
   &lt;div&gt;
   &lt;p&gt;Disability advocates call this “inspiration porn.” It’s condescending. Why should you be amazed that Candy wants to do something with her life?&lt;/p&gt;
   &lt;/div&gt;
  &lt;/div&gt;</code></pre>
  </div>
  </div>
  </p>