Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

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.

  1. Start with the event target. event.target is where the click originated. If the user clicks a nested icon or span inside the button, that child—not the button—is the target.
  2. Find the trigger. closest() walks from the target toward its ancestors and returns the nearest matching trigger. A direct event.target.matches() check can miss clicks on nested content.
  3. 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.
  4. Resolve and validate the panel. The button’s aria-controls identifies its panel. The example checks that the panel exists and is inside the same accordion before changing anything.
  5. Change both states together. The button’s aria-expanded value and the panel’s hidden property 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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().

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-controls points to the intended panel and that IDs are unique.
  • After opening and closing a panel, confirm that aria-expanded and 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.