aria-level
attribute if needed (for example, if the slice is part of a page where the title of the slice is an H3, not an H2. In that case the accordion headings should go down one level as well.)aria-expanded
attribute. This attribute tells the SR that the panel controlled by this button, and which must come after the button in the DOM, is either expanded or collapsed.aria-expanded
is set to true
. It is set to false
when the panel is collapsed.aria-hidden
set to either true
(when the panel is collapsed) or false
(when the panel is open).aria-controls
attribute telling the SR what panel it controls. And each panel has an ID referenced in its button’s aria-controls
, and an aria-labelledby
attribute referencing the ID of the button, allowing the SR to know that this panel is labelled by the content of that button.aria-controls
does not really have any browser support and can therefore be omitted altogether. I just have it included in the script anyway.data-accordion
attribute set on it.data-accordion-heading
,data-accordion-panel
attribute.allCollapsed
property (true
or false
);showOneAnswerAtATime
attribute (true
or false
);withControls
property is used to enable or disable that (true
or false
).
<div data-accordion class="c-accordion">
<h3 data-accordion-heading class="c-accordion__heading" aria-level="">
Test tracking
</h3>
<div data-accordion-panel class="c-accordion__panel">
Only use them when the content is appropriate. If the panels are too long, the user will end up scrolling too much (esp. on mobile), which negates the whole purpose of using an accordion. For very long panels, using links to separate pages is preferred.
<p>
If you use accordions to replace page steps such as checkout form steps, make sure you stop the default browser BACK button behavior so that the BACK button would move up a step in the accordion, because this is the behavior that most users expect, as
tests show.
</p>
</div>
<h3 data-accordion-heading class="c-accordion__heading" aria-level="">
What's a headless CMS?
</h3>
<div data-accordion-panel class="c-accordion__panel">
<a href="#">LINK</a> The default markup is enhanced using JavaScript so that the buttons are added to the headings. The buttons should NOT be in the headings by default. The markup should always consider what the component or content would look
like if no Javascript is enabled and therefore the accordion is not functional. The content is most likely going to be all visible — either as a series of panels with headings or a definition list, depending on the type of content.
</div>
<h3 data-accordion-heading class="c-accordion__heading" aria-level="">
What's special or different about Prismic?
</h3>
<div data-accordion-panel class="c-accordion__panel">
The buttons need to specify which panels they control using <code>aria-controls</code>, and they need to reflect the state of the panel (collapsed/expanded) using <code>aria-expanded="true/false"</code>. The panel itself also should have <code>aria-hidden</code> set to true or false if it is hidden or shown, respectively.
<p>
The accordion needs to be able to trap keyboard focus when the user is IN the accordion and uses the arrow keys to traverse the items in it. Tabbing out of the accordion must be enabled.
</p>
</div>
<h3 data-accordion-heading class="c-accordion__heading" aria-level="">
How is Prismic different from other CMSs?
</h3>
<div data-accordion-panel class="c-accordion__panel">
You can allow one or more panels to be always collapsed or expanded. You can also set a panel to be open by default (e.g. the first one). All this functionality can be tied to the markup using data attributes as flags.
</div>
</div>
/*********************************************************\
* Accordion component-specific styles.
*********************************************************/
.c-accordion__heading {
font-size: calc(1rem * var(--text-min-l));
@media screen and (min-width: 40rem) {
font-size: calc(calc(1rem * var(--text-min-l)) + (var(--text-max-l) - var(--text-min-l)) * (100vw - 40rem) / (80 - 40));
}
@media screen and (min-width: 80rem) {
font-size: calc(1rem * var(--text-max-l));
}
.accordion-js & {
margin: 0;
}
>button {
display: block;
font: inherit;
font-size: inherit;
font-weight: 500;
width: 100%;
height: 100%;
background-color: var(--color--secondary);
text-align: left;
line-height: 1.2;
padding: var(--c-padding);
padding-right: 80px;
position: relative;
border: 1px solid transparent;
transition: outline 0.1s linear;
border-radius: 8px;
margin-bottom: var(--c-padding);
&:focus {
outline: var(--focus-outline);
z-index: 1; // to ensure the outline isn't cut off where it overlaps with the next item below
}
&:focus:not(:focus-visible) {
outline: none;
}
&.focus:not(.focus-visible) {
outline: none;
}
&[aria-expanded="true"] {
margin-bottom: 0;
border-radius: 8px 8px 0 0;
}
}
}
.c-accordion__panel {
margin-bottom: 4rem; // no-js fallback
.accordion-js & {
margin-bottom: 0;
padding: calc(var(--c-padding) / 3) var(--c-padding) var(--c-padding);
padding-right: 4rem;
background-color: var(--color--secondary);
&[aria-hidden="true"] {
margin-bottom: 0;
}
&[aria-hidden="false"] {
border-radius: 0 0 8px 8px;
margin-bottom: var(--c-padding);
}
}
}
/* Styles for the accordion icon */
.c-accordion .accordion-icon {
display: block !important; // to override aria-hidden
position: absolute;
width: 0.75rem;
height: 0.5rem;
top: 50%;
right: 1em;
transform: translateY(-50%);
transform-origin: 50% 50%;
transition: all 0.1s linear;
}
.c-accordion [aria-expanded="true"] .accordion-icon {
-ms-transform: translateY(-50%) rotate(180deg);
transform: translateY(-50%) rotate(180deg);
}
.c-accordion [aria-hidden="true"] {
display: none;
}
.c-accordion [aria-hidden="false"] {
display: block !important;
}
"use strict";
if (typeof Object.assign != "function") {
// Must be writable: true, enumerable: false, configurable: true
Object.defineProperty(Object, "assign", {
value: function assign(target, varArgs) {
// .length of function is 2
if (target == null) {
// TypeError if undefined or null
throw new TypeError(
"Cannot convert undefined or null to object"
);
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) {
// Skip over if undefined or null
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (
Object.prototype.hasOwnProperty.call(
nextSource,
nextKey
)
) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}
// add utilities
var util = {
keyCodes: {
UP: 38,
DOWN: 40,
LEFT: 37,
RIGHT: 39,
HOME: 36,
END: 35,
ENTER: 13,
SPACE: 32,
DELETE: 46,
TAB: 9
},
generateID: function (base) {
return base + Math.floor(Math.random() * 999);
},
getDirectChildren: function (elm, selector) {
return Array.prototype.filter.call(elm.children, function (child) {
return child.matches(selector);
});
}
};
(function (w, doc, undefined) {
var ARIAaccOptions = {
showOneAnswerAtATime: true,
allCollapsed: true,
withControls: true,
// the following needs to be an SVG icon
// we will be dynamically inserting this icon into the buttons in this script
// make sure you add the class `accordion-icon` to it
icon:
'<svg class="accordion-icon" width="12" height="8" aria-hidden="true" focusable="false" viewBox="0 0 12 8"><g fill="none"><path fill="#000" d="M1.41.59l4.59 4.58 4.59-4.58 1.41 1.41-6 6-6-6z"/><path d="M-6-8h24v24h-24z"/></g></svg>'
}
/**
* ARIA Accordion
* Creates a tab list to toggle the visibility of
* different subsections of a document.
*
* Author: Sara Soueidan
* Version: 0.2.0
*/
var ARIAaccordion = function (inst, options) {
var _options = Object.assign(ARIAaccOptions, options);
var el = inst;
var accordionHeadings = el.querySelectorAll("[data-accordion-heading]");
var accordionPanels = el.querySelectorAll("[data-accordion-panel]");
var controlsWrapper;
var expandButton;
var collapseButton;
var accID = util.generateID('c-accordion-');
var init = function () {
// if you have any functionality in CSS that needs JS to be activated
// this class added to the accordion container works as a JS hook to announce JS is enabled
// in the CSS, we have a part that adds borders and paddings to the headings, buttons and panels
// these borders and padding are only needed if the content turns into an accordion
el.classList.add("accordion-js");
setupAccordionHeadings(accordionHeadings);
setupAccordionPanels(accordionPanels);
if (_options.withControls) {
createControls();
}
}
var createControls = function () {
controlsWrapper = document.createElement("div");
controlsWrapper.setAttribute("data-accordion-controls", "");
el.prepend(controlsWrapper);
expandButton = document.createElement("button");
expandButton.setAttribute("data-accordion-control", "expand");
expandButton.setAttribute("aria-label", "Expand all panels");
expandButton.innerText = "Expand All";
controlsWrapper.appendChild(expandButton);
collapseButton = document.createElement("button");
collapseButton.setAttribute("data-accordion-control", "collapse");
collapseButton.setAttribute("aria-label", "Collapse all panels");
collapseButton.innerText = "collapse All";
controlsWrapper.appendChild(collapseButton);
// if we start out with an accordion whose panels are collapsed (as opposed to open)
// disable the Collapse All button
if (_options.allCollapsed) disableCollapseButton();
setupAccordionControls();
}
var setupAccordionControls = function () {
expandButton.addEventListener("click", function () {
// expand them all
Array.from(accordionHeadings).forEach(function (item, index) {
item.querySelector("button").setAttribute("aria-expanded", "true");
});
Array.from(accordionPanels).forEach(function (item, index) {
item.setAttribute("aria-hidden", "false");
});
disableExpandButton();
enableCollapseButton();
});
collapseButton.addEventListener("click", function () {
Array.from(accordionHeadings).forEach(function (item, index) {
item.querySelector("button").setAttribute("aria-expanded", "false");
});
Array.from(accordionPanels).forEach(function (item, index) {
item.setAttribute("aria-hidden", "true");
});
disableCollapseButton();
enableExpandButton();
});
}
var setupAccordionHeadings = function (accordionHeadings) {
Array.from(accordionHeadings).forEach(function (item, index) {
var $this = item;
let text = $this.innerText;
let headingButton = document.createElement("button");
headingButton.setAttribute("aria-expanded", "false");
headingButton.setAttribute("data-accordion-toggle", "");
headingButton.setAttribute("id", accID + '__heading-' + index);
headingButton.setAttribute("aria-controls", accID + '__panel-' + index);
headingButton.innerText = text;
$this.innerHTML = "";
$this.appendChild(headingButton);
headingButton.innerHTML += _options.icon;
headingButton.addEventListener("click", function (e) {
togglePanel(headingButton);
});
});
}
var setupAccordionPanels = function (accordionPanels) {
Array.from(accordionPanels).forEach(function (item, index) {
let $this = item;
$this.setAttribute("id", accID + '__panel-' + index);
$this.setAttribute("aria-labelledby", accID + '__heading-' + index);
$this.setAttribute("aria-hidden", "true");
});
}
var togglePanel = function (toggleButton) {
var thepanel = toggleButton.parentNode.nextElementSibling;
if (toggleButton.getAttribute("aria-expanded") == "true") {
toggleButton.setAttribute("aria-expanded", "false");
thepanel.setAttribute("aria-hidden", "true");
checkToggleCollapseButtonState();
checkToggleExpandButtonState();
} else {
if (_options.showOneAnswerAtATime) {
// Hide all answers
Array.from(accordionPanels).forEach((panel) => {
panel.setAttribute("aria-hidden", "true");
});
Array.from(accordionHeadings).forEach((heading) => {
heading.querySelector("button")
.setAttribute("aria-expanded", "false");
})
checkToggleCollapseButtonState();
checkToggleExpandButtonState();
}
// Show answer
toggleButton.setAttribute("aria-expanded", "true");
thepanel.setAttribute("aria-hidden", "false");
checkToggleCollapseButtonState();
checkToggleExpandButtonState();
}
}
var enableCollapseButton = function () {
if (collapseButton) collapseButton.removeAttribute("disabled");
}
var disableCollapseButton = function () {
if (collapseButton) collapseButton.setAttribute("disabled", "disabled");
}
var enableExpandButton = function () {
if (expandButton) expandButton.removeAttribute("disabled");
}
var disableExpandButton = function () {
if (expandButton) expandButton.setAttribute("disabled", "disabled");
}
var checkToggleExpandButtonState = function () {
var closedPanels = el.querySelectorAll(
'button[aria-expanded="false"]'
);
if (!closedPanels.length) {
disableExpandButton();
} else {
enableExpandButton();
}
}
var checkToggleCollapseButtonState = function () {
var openPanels = el.querySelectorAll(
'button[aria-expanded="true"]'
);
if (openPanels.length === 0) {
disableCollapseButton();
} else {
enableCollapseButton();
}
}
init.call(this);
return this;
}; // ARIAaccordion()
w.ARIAaccordion = ARIAaccordion;
})(window, document);
var accInstance = "[data-accordion]";
var els = document.querySelectorAll(accInstance);
var allAccs = [];
// Generate all accordion instances
for (var i = 0; i < els.length; i++) {
// var nAccs = new ARIAaccordion(els[i]);
var nAccs = new ARIAaccordion(els[i], { withControls: false });
allAccs.push(nAccs);
}
// import the accordion component styles (the actual accordion element)
@import "_c-accordion.scss";
.ps-accordion__img {
display: block;
margin: 0 auto calc(var(--c-padding) * 2);
background: var(--color--secondary);
}
title: Accordion
# notes: "You can have a note here; it will override the README.md notes"
context:
text: