Event bubbling lets a click on an accordion button travel up through its ancestors, so one listener on the accordion container can handle clicks from all its buttons. Refactoring to event delegation can simplify wiring and accommodate buttons added later, but the handler still needs to identify the right button and keep the accessible state in sync with panel visibility.
What event bubbling means
When a click starts on an element, the event can travel through the DOM from that target to its ancestors. A listener on an ancestor can respond to the child’s click during this bubbling phase. MDN describes bubbling as useful because it enables event delegation: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Event_bubbling.
For an accordion, that means a click on a header button can reach a listener registered on the accordion container. The container can then determine which button was activated and update its panel.
Before: one listener for every button
A direct-listener implementation finds each trigger and wires it separately:
#1 Best Overall
const triggers = document.querySelectorAll('[data-accordion-trigger]');
triggers.forEach((button) => {
button.addEventListener('click', () => {
const panel = document.getElementById(button.getAttribute('aria-controls'));
const isOpen = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', String(!isOpen));
panel.hidden = isOpen;
});
});
This is a valid choice for a static accordion: each button has its own handler. The wiring must be repeated if additional buttons are inserted later, and the setup is distributed across the controls rather than attached to one stable container.
After: delegate clicks from the accordion container
With delegation, register one bubbling-phase listener on the container. Resolve the nearest trigger from the click target, verify that it belongs to this accordion, then update the trigger and its panel together.
Rank #2
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 || !accordion.contains(panel)) return;
const willOpen = button.getAttribute('aria-expanded') !== 'true';
button.setAttribute('aria-expanded', String(willOpen));
panel.hidden = !willOpen;
});
addEventListener() is the standard listener-registration API. Unless configured otherwise, the listener handles the event in the bubbling phase; its options can instead specify capture behavior. See MDN’s addEventListener() reference.
- Start with the event target.
event.targetis where the click originated. If the user clicks a nested icon or span inside the button, that child—not the button—is the target. - Find the trigger.
closest()walks from the target toward its ancestors and returns the nearest matching trigger. A directevent.target.matches()check can miss clicks on nested content. - Keep the action inside the current accordion. The containment check ensures that the resolved trigger belongs to this container, which matters when accordions are nested or selectors match other parts of the page.
- Resolve and validate the panel. The button’s
aria-controlsidentifies its panel. The example checks that the panel exists and is inside the same accordion before changing anything. - Change both states together. The button’s
aria-expandedvalue and the panel’shiddenproperty are updated in one operation.
event.currentTarget is different: it is the element whose listener is currently running. In this handler, that is the accordion container. MDN explains the distinction between target and currentTarget in its Event.target reference.
How to keep the accordion accessible
Use a native button inside a heading
Make each accordion heading a semantic heading containing a native <button>. The button is the interactive control; avoid making a generic heading or container behave like a button without recreating its keyboard and accessibility behavior.
Connect each button and panel
Give every panel a stable, unique ID. Set the controlling button’s aria-controls to that ID. Set aria-expanded="true" while the panel is visible and false while it is collapsed. If the panel is exposed as a landmark region, give it role="region" and an aria-labelledby value pointing to its controlling button. These relationships follow the W3C ARIA Authoring Practices accordion pattern.
Rank #4
Preserve expected keyboard behavior
With a native button, Enter and Space activate the focused accordion header. Tab and Shift+Tab continue through focusable elements in the page’s normal sequence. The W3C pattern also describes optional arrow-key movement between headers, as well as Home and End navigation. Add those behaviors only when the interface calls for that richer interaction; they are not required to replace normal Tab navigation.
Derive visual styling from the same state
A CSS rule keyed to [aria-expanded="true"] can style an open header, while JavaScript updates the panel’s visibility. Keep these states synchronized: if the panel becomes visible but aria-expanded remains false, sighted users and assistive-technology users receive contradictory information.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallBest Value
When to choose delegation
| Consideration | Direct listeners | Delegated listener |
|---|---|---|
| Listener setup | One listener is attached to each button. | One listener is attached to the stable accordion container. |
| Buttons added later | New buttons need their own listener wiring. | New buttons inside the container can be handled without rebinding. |
| Identifying the control | The handler already has its button reference. | The handler resolves a button from event.target and its ancestors. |
| Where state logic lives | Behavior is wired per control. | Selector and state logic is centralized in the container handler. |
| Nested interactive content | Click handling stays on the button. | Check which trigger the event belongs to; only use propagation control for a deliberate interaction boundary. |
Delegation is a maintainability and dynamic-content choice, not a guaranteed performance improvement. No benchmark figure is established for this accordion scenario, so listener counts should not be translated into unsupported speed claims.
When not to stop propagation
Do not add stopPropagation() as a routine fix. Bubbling is what lets the container’s delegated listener receive the click. Stop propagation only when a nested interactive component has a deliberate reason to prevent an outer accordion action, and make that interaction boundary clear. MDN documents stopPropagation() as preventing an event from bubbling to other elements in the event path: Event.stopPropagation().
Quick Recap
Check the finished refactor
- Click the button text, its icon, and any nested span; each should operate the same panel.
- Verify that each trigger’s
aria-controlspoints to the intended panel and that IDs are unique. - After opening and closing a panel, confirm that
aria-expandedand panel visibility agree. - Insert a trigger and panel beneath the container after setup; confirm that delegation handles the new trigger.
- Test the finished accordion with the browsers and assistive technologies supported by the project. The W3C pattern presents an implementation pattern, not a substitute for testing the completed interface.
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.

