Event bubbling lets a click on a child element travel up through its ancestors, so one listener on an accordion container can handle clicks on all of its header buttons. The refactor replaces repeated per-button wiring with a delegated listener while keeping each button’s accessible state synchronized with its panel.
What event bubbling means
When a click starts on an element, the event travels through the DOM’s ancestor path. During the bubbling phase, listeners on those ancestors can respond. That behavior makes event delegation possible: instead of attaching a listener to every accordion button, attach one listener to a stable parent and identify which button the click came from. See MDN’s explanation of event bubbling and delegation.
Before: a listener on every accordion button
A straightforward implementation finds each button and registers a handler in a loop. This works for an accordion whose markup is already present, but the setup is repeated for every button. If more headers are inserted later, the code must run again to attach their handlers.
const buttons = document.querySelectorAll('[data-accordion-trigger]');
buttons.forEach((button) => {
button.addEventListener('click', () => {
const panel = document.getElementById(button.getAttribute('aria-controls'));
const willOpen = button.getAttribute('aria-expanded') !== 'true';
button.setAttribute('aria-expanded', String(willOpen));
panel.hidden = !willOpen;
});
});
The handler has a direct reference to its button, so it does not need to work out which descendant was clicked. Its limitation is the wiring: buttons added after the query do not receive a listener automatically.
#1 Best Overall
After: delegate clicks from the accordion container
Put a listener on the stable accordion element. In the handler, resolve the nearest trigger button, check that it belongs to this accordion, then update the button and its associated panel together.
const accordion = document.querySelector('[data-accordion]');
accordion.addEventListener('click', (event) => {
if (!(event.target instanceof Element)) return;
const button = event.target.closest('[data-accordion-trigger]');
if (!button || !accordion.contains(button)) return;
const panelId = button.getAttribute('aria-controls');
const panel = panelId ? document.getElementById(panelId) : null;
if (!panel) return;
const willOpen = button.getAttribute('aria-expanded') !== 'true';
button.setAttribute('aria-expanded', String(willOpen));
panel.hidden = !willOpen;
});
Why resolve the nearest button?
A click may land on a nested icon or span inside a button. In that case, event.target is the nested element, not the button. Calling closest('[data-accordion-trigger]') walks upward from the target to find the intended trigger. The containment check prevents this accordion from acting on a matching button outside its own subtree.
Rank #2
What target and currentTarget mean
event.target is where the event began; event.currentTarget is the element whose listener is currently running. With the delegated listener above, target may be an icon within a button, while currentTarget is the accordion container. MDN highlights target as a commonly used property in delegation: Event.target and Event.currentTarget.
Why dynamically inserted buttons work
The listener stays on the accordion container. A new trigger added beneath that container can therefore be handled when its click bubbles up, without registering another listener. The example still expects the new button and its panel to have the required attributes and relationship.
Keep accordion behavior and accessibility in sync
Use a semantic heading containing a native button for each header. Give each panel a stable ID and set the button’s aria-controls to that ID. The button’s aria-expanded must be true while its panel is visible and false while the panel is collapsed. A panel may also be exposed as a labelled region, with aria-labelledby referring to its controlling button. These relationships follow the W3C ARIA Authoring Practices accordion pattern.
Example markup
<section data-accordion>
<h3>
<button id="shipping-trigger"
type="button"
data-accordion-trigger
aria-expanded="false"
aria-controls="shipping-panel">
Shipping details <span aria-hidden="true">⌄</span>
</button>
</h3>
<div id="shipping-panel"
role="region"
aria-labelledby="shipping-trigger"
hidden>
<p>Delivery information goes here.</p>
</div>
</section>
The HTML hidden property controls whether the panel is displayed; the handler changes it in the same operation as aria-expanded. Visual styling can also derive from the ARIA state:
Rank #4
[data-accordion-trigger][aria-expanded="true"] {
font-weight: 700;
}
A common failure is to change the visual appearance or hide the panel without updating aria-expanded. That leaves assistive technology with a state that contradicts what is visible. Keep the announced state, panel visibility, and any related CSS state aligned.
Keyboard behavior
Because the header is a native button, Enter and Space activate it. The W3C pattern specifies that these keys expand a collapsed panel; an implementation that allows collapsing can use them to collapse an expanded one as well. Tab and Shift+Tab continue through the page’s normal focus order. Arrow, Home, and End movement between accordion headers are optional enhancements rather than a requirement of the basic pattern. If you add them, implement and test the focus behavior deliberately instead of changing the normal tab sequence by accident.
Best Value
Choose the listener pattern that fits the interface
| Consideration | Listener on each button | Delegated listener |
|---|---|---|
| Listener placement | One listener per button. | One listener on a stable ancestor. |
| Buttons added later | Need a new listener or a rerun of the binding code. | Handled when inserted under the listening container, provided markup relationships are valid. |
| Identifying the control | The handler already has the button reference. | Resolve the trigger from event.target, then verify it belongs to the container. |
| Maintenance | Behavior and binding are attached across individual controls. | Click handling is centralized, with selector and state logic in one place. |
Delegation is a useful maintainability and dynamic-content choice, not a guarantee of faster performance in every interface. No measured performance figure is established for this particular accordion refactor, so choose based on how the component is built and maintained rather than assuming a speed gain.
Handling single-open accordions and nested widgets
When only one panel may stay open
If the design allows only one expanded panel, close the other panel as part of the same delegated operation. For each other trigger within this accordion, set aria-expanded to false and set its controlled panel’s hidden property to true; then apply the new state to the clicked trigger and its panel. Do not update only the clicked button: the state of every panel you close must also be reflected in its control.
When a nested component should not activate the accordion
Do not use stopPropagation() as a general fix for event handling. Bubbling is what lets the delegated listener receive clicks. A nested interactive widget may have a deliberate interaction boundary where its click must not trigger an outer accordion action; use propagation control only for that defined case and document the boundary. MDN describes stopPropagation() as preventing an event from bubbling to other elements.
Test the refactor before shipping
- Click the button text and any nested icon or span; confirm each click resolves to the same trigger.
- Add a new header beneath the accordion after setup; confirm its click is handled and its control points to an existing panel.
- Check that opening and closing a panel updates both
aria-expandedand panel visibility. - Use Enter and Space on a focused header, and confirm Tab and Shift+Tab follow the page’s normal focus order.
- If the accordion permits one panel at a time, confirm closing another panel also updates that other button’s state.
- Test the finished component in the project’s supported browsers and assistive-technology combinations. The W3C pattern presents its example as illustrative; it is not a substitute for testing the implementation in its intended environment.
Use addEventListener() to register the container listener; it is the standard event-listener API and supports options such as capture, while the example relies on the default bubbling phase. See MDN’s addEventListener() reference.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteQuick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




