<style>
/**
* Hide the menu during the 'no-js' state
* to solve Cumulative Layout Shift.
*/
[data-state='no-js'] .disclosure__content {
display: none;
}
</style>
<div class="disclosure js-disclosure" data-state="no-js">
<button aria-expanded="false" class="disclosure__button js-disclosure-btn" aria-controls="content" disabled title="This button becomes functional once JavaScript is active">
Disclosure trigger
<svg class="disclosure__icon" focusable="false" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 6"><polygon points="8.97 0 5.01 3.9 1 0.03 0 1.03 4.96 6 10 1.08 8.97 0"></polygon></svg>
</button>
<div class="disclosure__content" id="content">
<!-- Additional content goes here -->
<p>Revealed content!</p>
</div>
</div>
<noscript>
<!-- Show all content if no JS (avoids CLS) -->
<style>
[data-state='no-js'] .disclosure__content {
display: block;
}
</style>
</noscript>
// Base styles
body {
margin: 0;
padding: 2rem;
font-size: 1.5rem;
font-family: sans-serif;
}
//-----------------
// Disclosure styles
.disclosure {
border: 1px solid #ccc;
padding: 10px;
max-width: 50rem;
}
.disclosure__content {
margin-top: 10px;
}
.disclosure__icon {
height: 1em;
width: 1em;
fill: currentcolor;
transition: transform 0.3s ease-out;
}
.disclosure__button {
background-color: #222;
padding: 0.25em 0.5em;
color: #fff;
border: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1ch;
&:focus-visible {
outline: solid orange 3px;
outline-offset: 3px;
}
&[aria-expanded="false"] {
+ .disclosure__content {
display: none;
}
}
&[aria-expanded="true"] {
+ .disclosure__content {
display: block;
}
.disclosure__icon {
transform: rotate(-180deg);
}
}
}
View Compiled
/** Technical Requirements
* 1 * Must do a null check for component's presence on the page
* 2 * Must update the data-state variable and button attributes on load
* 3 * Must toggle aria-expanded boolean on click
* 4 * No requirement for closing other disclosures in a set when one is open
*/
const disclosures = document.querySelectorAll('.js-disclosure');
function toggleDisclosure() {
const isExpanded = this.getAttribute('aria-expanded') === 'true';
this.setAttribute('aria-expanded', !isExpanded);
}
function init() {
// early return if no disclosures present on page
if (!disclosures.length) {
return
}
// update state not JS has loaded
disclosures.forEach(component => {
component.dataset.state = 'ready';
const disclosureButtons = component.querySelectorAll('.js-disclosure-btn');
disclosureButtons.forEach(btn => {
// initialise button attributes now JS has loaded
btn.removeAttribute('title');
btn.removeAttribute('disabled');
// listen for clicks
btn.addEventListener('click', toggleDisclosure);
});
});
}
document.addEventListener(
'DOMContentLoaded',
() => {
init();
});
This Pen doesn't use any external JavaScript resources.