Accordion (Multiselect)
Accordion (Multiselect)
Turn on a screen reader to experience this example in action.
Non-functional link, for testing purposes
HTML Source Code
<!---
This component has been adapted from an example provided by the W3C, in accordance with the W3C Software and Document License https://www.w3.org/copyright/software-license-2023/
-->
<div class="dqu-example">
<div id="accordionGroup" class="accordion">
<h2>
<button type="button"
aria-expanded="true"
class="accordion-trigger"
aria-controls="sect1"
id="accordion1id">
<span class="accordion-title">
Prospective Students
<span class="accordion-icon"></span>
</span>
</button>
</h2>
<div id="sect1"
role="region"
aria-labelledby="accordion1id"
class="accordion-panel">
<ul>
<li><a href="#">Prospective Students Home Page</a></li>
<li><a href="#">Admissions</a></li>
<li><a href="#">Financing Your Education</a></li>
</ul>
</div>
<h2>
<button type="button"
aria-expanded="false"
class="accordion-trigger"
aria-controls="sect2"
id="accordion2id">
<span class="accordion-title">
Students
<span class="accordion-icon"></span>
</span>
</button>
</h2>
<div id="sect2"
role="region"
aria-labelledby="accordion2id"
class="accordion-panel"
hidden="">
<ul>
<li><a href="#">Students Home Page</a></li>
<li><a href="#">Records</a></li>
<li><a href="#">Student Life</a></li>
<li><a href="#">Student Organizations</a></li>
<li><a href="#">Technology</a></li>
<li><a href="#">Library</a></li>
</ul>
</div>
<h2>
<button type="button"
aria-expanded="false"
class="accordion-trigger"
aria-controls="sect3"
id="accordion3id">
<span class="accordion-title">
Faculty
<span class="accordion-icon"></span>
</span>
</button>
</h2>
<div id="sect3"
role="region"
aria-labelledby="accordion3id"
class="accordion-panel"
hidden="">
<ul>
<li><a href="#">Faculty Home</a></li>
<li><a href="#">Directory</a></li>
<li><a href="#">News</a></li>
<li><a href="#">Publications</a></li>
</ul>
</div>
<h2>
<button type="button"
aria-expanded="false"
class="accordion-trigger"
aria-controls="sect4"
id="accordion4id">
<span class="accordion-title">
Alumni
<span class="accordion-icon"></span>
</span>
</button>
</h2>
<div id="sect4"
role="region"
aria-labelledby="accordion4id"
class="accordion-panel"
hidden="">
<ul>
<li><a href="#">Alumni Home Page</a></li>
<li><a href="#">Benefits and Services</a></li>
<li><a href="#">Giving</a></li>
<li><a href="#">Alumni Association</a></li>
<li><a href="#">Volunteer Opportunities</a></li>
</ul>
</div>
</div>
</div>
JavaScript Source Code
Dependencies:
- JQuery
//This component has been adapted from an example provided by the W3C, in accordance with the W3C Software and Document License https://www.w3.org/copyright/software-license-2023/
if( typeof TabAccordion === "undefined")
{
var TabAccordions = [];
class TabAccordion {
static openedEles = new Map();
constructor(domNode, multiple ) {
if (typeof multiple == "undefined") multiple=true;
if (typeof multiple == "boolean") this.multiple = multiple;
this.rootEl = domNode;
this.buttonEl = this.rootEl.querySelector('button[aria-expanded]');
if ( this.buttonEl == null)
{
console.log("button and/or aria-expanded attribute is missing.");
return null;
}
const controlsId = this.buttonEl.getAttribute('aria-controls');
this.contentEl = document.getElementById(controlsId);
this.open = this.buttonEl.getAttribute('aria-expanded') === 'true';
this.closeActiveTab();
// add event listeners
this.buttonEl.addEventListener('click', this.onButtonClick.bind(this));
}
onButtonClick() {
this.toggle(!this.open);
}
toggle(open) {
// don't do anything if the open state doesn't change
if (open === this.open) {
return;
}
// update the internal state
this.open = open;
// handle DOM updates
this.buttonEl.setAttribute('aria-expanded', `${open}`);
if (open) {
this.contentEl.removeAttribute('hidden');
} else {
this.contentEl.setAttribute('hidden', '');
}
this.closeActiveTab();
}
closeActiveTab()
{
if (this.multiple ) return;
var group = this.rootEl.closest('.accordion') || this.rootEl.parentElement;
if(this.open)
{
var prev = TabAccordion.openedEles.get(group);
if( prev != null )
{
prev.contentEl.setAttribute('hidden', '');
prev.open = false;
prev.buttonEl.setAttribute('aria-expanded', 'false');
}
TabAccordion.openedEles.set(group, this);
}
else
{
if(this == TabAccordion.openedEles.get(group))
TabAccordion.openedEles.delete(group);
}
}
closeOthers()
{
const accordions = document.querySelectorAll('.accordion h2');
TabAccordions.forEach((accordionEl) => {
let tab = accordionEl;
if (tab.contentEl.id !== this.contentEl.id)
tab.close();
});
}
// Add public open and close methods for convenience
open() {
this.toggle(true);
}
close() {
this.toggle(false);
}
}
// init accordions
const accordions = document.querySelectorAll('.accordion h2');
accordions.forEach((accordionEl) => {
var tab = new TabAccordion(accordionEl);
TabAccordions.push(tab);
});
}
CSS Source Code
/*
Accordion (Single-Select) — Restyled
Deque University ARIA Component
Design tokens from: Deque University Redesign (Bobby's Figma)
This component has been adapted from an example provided by the W3C,
in accordance with the W3C Software and Document License
https://www.w3.org/copyright/software-license-2023/
*/
/* ========================================
Design Tokens
======================================== */
.accordion {
--dqu-bg-primary: #fcfaf8;
--dqu-bg-secondary: #f6f3ed;
--dqu-text-primary: #21201e;
--dqu-border-primary: #beb7aa;
--dqu-border-secondary: #8c827d;
--dqu-interactive: #2e5f7a;
--dqu-interactive-light: rgba(46, 95, 122, 0.08);
--dqu-interactive-hover: #3a7a9a;
--dqu-interactive-active: #254c62;
--dqu-font-family: "Noto Sans", sans-serif;
--dqu-radius: 8px;
}
/* ========================================
Container
======================================== */
.accordion {
margin: 0;
padding: 0;
border: 1px solid var(--dqu-border-secondary);
border-radius: var(--dqu-radius);
width: 24em;
font-family: var(--dqu-font-family);
background: var(--dqu-bg-primary);
overflow: hidden;
}
.accordion:focus-within {
border-color: var(--dqu-interactive);
box-shadow: 0 0 0 1px var(--dqu-interactive);
}
.accordion h2 {
margin: 0;
padding: 0;
}
.accordion > * + * {
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
/* ========================================
Trigger Button
======================================== */
.accordion-trigger {
background: var(--dqu-interactive);
color: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
font-family: var(--dqu-font-family);
font-weight: 500;
font-size: 1rem;
line-height: 1.5;
width: 100%;
margin: 0;
padding: 12px 16px;
border: none;
border-radius: 0;
cursor: pointer;
text-align: left;
transition: background-color 0.15s ease;
}
/* Hover and focus-visible: invert — light teal background + dark teal text
+ inset teal ring. Both outline and inset box-shadow used so the ring
survives any global outline:none rules on the host page (WCAG 2.4.11). */
.accordion-trigger:hover,
.accordion-trigger:focus-visible {
background: var(--dqu-interactive-light);
color: var(--dqu-interactive);
outline: 3px solid var(--dqu-interactive) !important;
outline-offset: -3px;
box-shadow: inset 0 0 0 3px var(--dqu-interactive);
z-index: 1;
}
/* Remove default focus outline for mouse users */
.accordion-trigger:focus:not(:focus-visible) {
outline: none !important;
}
.accordion:focus-within h2 {
/* No special style needed — focus-visible on trigger handles it */
}
/* First / last triggers absorb the container's interior corner radius
(8px container - 1px border = 7px) so focus/hover backgrounds curve
to match the rounded card. Last-child scoped to collapsed state so
the trigger stays square-bottom when joined to its expanded panel. */
.accordion > h2:first-of-type .accordion-trigger {
border-radius: 7px 7px 0 0;
}
.accordion > h2:last-of-type .accordion-trigger[aria-expanded="false"] {
border-radius: 0 0 7px 7px;
}
/* ========================================
Trigger Title
======================================== */
.accordion-title {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
pointer-events: none;
border: transparent 2px solid;
border-radius: 4px;
padding: 0;
outline: none;
}
.accordion-trigger:focus .accordion-title {
border-color: transparent; /* Using focus-visible on trigger instead */
}
/* ========================================
Chevron Icon
======================================== */
.accordion-icon {
border: solid #ffffff;
border-width: 0 2px 2px 0;
height: 0.5rem;
width: 0.5rem;
pointer-events: none;
position: absolute;
right: 1.25em;
top: 50%;
transform: translateY(-60%) rotate(45deg);
transition: transform 0.2s ease;
flex-shrink: 0;
}
.accordion-trigger:hover .accordion-icon,
.accordion-trigger:focus-visible .accordion-icon {
border-color: var(--dqu-interactive);
}
.accordion-trigger[aria-expanded="true"] .accordion-icon {
transform: translateY(-50%) rotate(-135deg);
}
/* ========================================
Panel Content
======================================== */
.accordion-panel {
margin: 0;
background: var(--dqu-bg-primary);
}
.accordion-panel[hidden] {
display: none;
}
.accordion-panel ul {
margin: 0;
padding: 0;
}
.accordion-panel li {
list-style-type: none;
background: var(--dqu-bg-primary);
border-bottom: 1px solid var(--dqu-border-secondary);
margin: 0;
padding: 0;
}
.accordion-panel li:last-child {
border-bottom: none;
}
.accordion-panel li a {
display: block;
padding: 10px 16px;
color: var(--dqu-interactive);
text-decoration: underline;
font-size: 0.9375rem;
line-height: 1.5;
transition: background-color 0.15s ease;
}
/* Hover and focus-visible share one indicator — light teal background +
inset teal outline — so keyboard and pointer users get the same
high-contrast affordance (WCAG 2.4.11). */
.accordion-panel li a:hover,
.accordion-panel li a:focus-visible {
outline: 3px solid var(--dqu-interactive) !important;
outline-offset: -3px;
background: var(--dqu-interactive-light);
text-decoration: underline;
}
/* ========================================
Legacy / Override Cleanup
======================================== */
.dqu-example button {
border-style: none;
}
.accordion button::-moz-focus-inner {
border: 0;
}
/* Legacy tab styles — kept for compatibility but restyled */
.accordion div.tab:focus {
background-color: var(--dqu-interactive-light);
outline: 2px solid var(--dqu-interactive) !important;
outline-offset: -2px;
}
.accordion > li:hover {
cursor: pointer;
}
.accordion div.tab {
background: var(--dqu-bg-secondary);
border: 1px solid var(--dqu-border-secondary);
color: var(--dqu-text-primary);
padding: 3px 6px;
}
.tablist div.tab.active {
font-weight: bold;
}
.accordion-panel .inactive {
display: none;
}