Expand/Collapse
Expand/Collapse
This pattern creates a button that toggles an element as hidden (collapsed) or not hidden (expanded). The element's current state and changes in the element's state are communicated to screen reader users. The HTML 5 elements <details> and <summary> provide this kind of functionality natively, but support for those elements is not universal. There is an option to use <details> and <summary> for this pattern in browsers that support them, or this pattern can be configured to use only generic <div> elements.
Turn on a screen reader to experience this example in action.
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="expanderGroup" class="expander">
<h2>
<button type="button"
aria-expanded="false"
class="expander-trigger"
aria-controls="sect1"
id="expander1id">
<span class="expander-title">
The Gettysburg Address, by Abraham Lincoln
<span class="expander-icon"></span>
</span>
</button>
</h2>
<div id="sect1"
role="region"
aria-labelledby="expander1id"
class="expander-panel " hidden>
<p>Four score and seven years ago our fathers brought forth
on this continent, a new nation, conceived in Liberty, and
dedicated to the proposition that all men are created equal.</p>
<p>Now we are engaged in a great civil war, testing whether that
nation, or any nation so conceived and so dedicated, can long endure.</p>
<p>We are met on a great battle-field of that war. We have come to dedicate
a portion of that field, as a final resting place for those who here gave
their lives that that nation might live. It is altogether fitting and proper
that we should do this.</p>
<p>But, in a larger sense, we can not dedicate — we can not consecrate
— we can not hallow — this ground. The brave men, living and dead,
who struggled here, have consecrated it, far above our poor power to add or
detract. The world will little note, nor long remember what we say here, but
it can never forget what they did here. It is for us the living, rather, to
be dedicated here to the unfinished work which they who fought here have thus
far so nobly advanced. It is rather for us to be here dedicated to the great
task remaining before us — that from these honored dead we take increased
devotion to that cause for which they gave the last full measure of devotion
— that we here highly resolve that these dead shall not have died in vain
— that this nation, under God, shall have a new birth of freedom —
and that government of the people, by the people, for the people, shall not
perish from the earth.</p>
</div>
</div>
<!---
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>
JavaScript Source Code
if( typeof TabExpander === "undefined")
{
var tabExpanders = [];
class TabExpander {
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('.expander') || this.rootEl.parentElement;
if(this.open)
{
var prev = TabExpander.openedEles.get(group);
if( prev != null )
{
prev.contentEl.setAttribute('hidden', '');
prev.open = false;
prev.buttonEl.setAttribute('aria-expanded', 'false');
}
TabExpander.openedEles.set(group, this);
}
else
{
if(this == TabExpander.openedEles.get(group))
TabExpander.openedEles.delete(group);
}
}
closeOthers()
{
const expanders = document.querySelectorAll('.expander h2');
tabExpanders.forEach((expanderEl) => {
let tab = expanderEl;
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 expanders
const expanders = document.querySelectorAll('.expander h2');
expanders.forEach((expanderEl) => {
var tab = new TabExpander(expanderEl);
tabExpanders.push(tab);
});
}
//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/
CSS Source Code
/*
Expand/Collapse — Restyled
Deque University ARIA Component
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/
*/
.expander {
--dqu-bg-primary: #fcfaf8;
--dqu-border-secondary: #8c827d;
--dqu-interactive: #2e5f7a;
--dqu-interactive-hover: #3a7a9a;
--dqu-interactive-light: rgba(46, 95, 122, 0.08);
--dqu-text-primary: #21201e;
--dqu-font-family: "Noto Sans", sans-serif;
--dqu-radius: 8px;
}
.expander {
margin: 0;
padding: 0;
border: 1px solid var(--dqu-border-secondary);
border-radius: var(--dqu-radius);
font-family: var(--dqu-font-family);
background: var(--dqu-bg-primary);
overflow: hidden;
}
.expander:focus-within {
border-color: var(--dqu-interactive);
box-shadow: 0 0 0 1px var(--dqu-interactive);
}
.expander h2 {
margin: 0;
padding: 0;
}
.expander > * + * {
border-top: 1px solid var(--dqu-border-secondary);
}
.expander-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;
cursor: pointer;
text-align: left;
transition: background-color 0.15s ease;
}
.expander-trigger:hover {
background: var(--dqu-interactive-hover);
}
.expander-trigger:focus-visible {
outline: none !important;
box-shadow: inset 0 0 0 3px #ffffff;
background: var(--dqu-interactive-hover);
z-index: 1;
}
.expander-trigger:focus:not(:focus-visible) {
outline: none !important;
}
.expander > *:first-child .expander-trigger,
.expander > *:first-child {
border-radius: 7px 7px 0 0;
}
.expander > *:last-child .expander-trigger,
.expander > *:last-child {
border-radius: 0 0 7px 7px;
}
/* When the trigger is the only one in the expander and it is
collapsed, round all four corners so its bottom edge follows
the container's rounded interior. (The hidden panel sibling
prevents :first-child:last-child from matching.) */
.expander > h2:only-of-type .expander-trigger[aria-expanded="false"] {
border-radius: 7px;
}
.dqu-example button {
border-style: none;
}
.expander button::-moz-focus-inner {
border: 0;
}
.expander-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;
}
.expander-trigger:focus .expander-title {
border-color: transparent;
}
.expander-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;
}
.expander-trigger:hover .expander-icon {
border-color: #ffffff;
}
.expander-trigger[aria-expanded="true"] .expander-icon {
transform: translateY(-50%) rotate(-135deg);
}
.expander-panel {
margin: 0;
padding: 16px;
line-height: 1.7;
background: var(--dqu-bg-primary);
color: var(--dqu-text-primary);
font-family: var(--dqu-font-family);
font-size: 0.9375rem;
}
.expander-panel[hidden] {
display: none;
}
.expander-panel .inactive {
display: none;
}