CHAPTER 4 OF 5

4. Build a working disclosure

Your goal: connect a native button to content and keep its state accurate.

Try the control

Try it: activate Workshop details with a pointer, Enter, or Space. Tab into the revealed link, then move back to the button to close it.

What to bring

Bring a laptop if you have one. Printed materials are also available.

Browse all accessibility guides

Connect state, relationship, and visibility

aria-expanded reports the state. We include aria-controls to reference the panel. JavaScript updates the state and the HTML hidden property together. The relationship attribute alone does not toggle anything.

HTML before enhancement: readable content and a hidden control
<!-- FALLBACK: The content is visible without JavaScript; the inactive control stays hidden. --> <div data-aria-disclosure> <!-- BUTTON: Native keyboard behavior and visible text supply the role and accessible name. --> <button type="button" class="button" aria-expanded="true" aria-controls="workshop-details" hidden>Workshop details</button> <!-- RELATIONSHIP: The ID matches aria-controls; the attribute itself does not hide or show this panel. --> <div id="workshop-details"> <h2>What to bring</h2> <p>Bring a laptop if you have one. Printed materials are also available.</p> <p><a href="/guides.html">Browse all accessibility guides</a></p> </div> </div>

The initial markup reports expanded content and keeps the inactive button hidden. After its click listener is ready, the script collapses the panel, sets aria-expanded="false", and reveals the button.

The synchronization function; the complete controller is on the example page
function setOpen(open) { panel.hidden = !open; button.setAttribute("aria-expanded", String(open)); }
Sources and further reading