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 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/
-->
JavaScript Source Code
if( typeof TabExpander === "undefined")
{
var tabExpanders = [];
class TabExpander {
static openedEle = null;
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;
if(this.open)
{
if( TabExpander.openedEle != null )
{
TabExpander.openedEle.contentEl.setAttribute('hidden', '');
TabExpander.openedEle.open = false;
TabExpander.openedEle.buttonEl.setAttribute('aria-expanded', 'false');
}
TabExpander.openedEle = this;
}
else
{
if(this == TabExpander.openedEle)
TabExpander.openedEle = null;
}
}
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
/*
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 {
margin: 0;
padding: 0;
border: 2px solid hsl(0deg 0% 52%);
border-radius: 7px;
}
.expander h2 {
margin: 0;
padding: 0;
}
.expander:focus-within {
border-color: hsl(216deg 94% 43%);
}
.expander:focus-within h2 {
background-color: hsl(0deg 0% 97%);
}
.expander > * + * {
border-top: 1px solid hsl(0deg 0% 52%);
}
.expander-trigger {
background: #375898;
color: white;
display: block;
font-weight: bold;
font-size: 1rem;
width: 100%;
margin: 0;
padding: 10px;
position: relative;
text-align: left;
outline: none;
}
.expander-trigger:focus,
.expander-trigger:hover {
background: hsl(216deg 94% 94%);
color: black;
}
.expander-trigger:focus {
/* outline: 4px solid transparent; */
}
.expander > *:first-child .expander-trigger,
.expander > *:first-child {
border-radius: 5px 5px 0 0;
}
.expander > *:last-child .expander-trigger,
.expander > *:last-child {
border-radius: 0 0 5px 5px;
}
button {
border-style: none;
}
.expander button::-moz-focus-inner {
border: 0;
}
.expander-title {
display: block;
pointer-events: none;
/* border: transparent 2px solid; */
border-radius: 5px;
padding: 0.25em;
outline: none;
}
.expander-trigger:focus .expander-title {
border-color: hsl(216deg 94% 43%);
}
.expander-icon {
border: solid currentcolor;
border-width: 0 2px 2px 0;
height: 0.5rem;
pointer-events: none;
position: absolute;
right: 2em;
top: 50%;
transform: translateY(-60%) rotate(45deg);
width: 0.5rem;
}
.expander-trigger:focus .expander-icon,
.expander-trigger:hover .expander-icon {
border-color: hsl(216deg 94% 43%);
}
.expander-trigger[aria-expanded="true"] .expander-icon {
transform: translateY(-50%) rotate(-135deg);
}
.expander-panel {
margin: 0;
padding: 15px;
line-height: 1.7em;
/* padding: 1em 1.5em; */
}
/* For Edge bug https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/4806035/ */
.expander-panel[hidden] {
display: none;
}
.expander div.tab:focus {
background-color: #566ac8;
/* outline: 2px solid #8cc63f; */
outline: 3px solid #467310;
}
.expander div.tab {
background: #375898;
border: 1px solid #fff;
color: #fff;
padding: 3px 6px;
}
.tablist div.tab.active {
font-weight: bold;
}
.expander-panel .inactive {
display: none;
}