Use document.querySelectorAll() to collect every current element with a class, then iterate the returned NodeList and call addEventListener() on each element. Read event.currentTarget inside the handler to identify the element whose listener ran.
function handleClick(event) {
const element = event.currentTarget;
console.log(element.dataset.id);
}
document.querySelectorAll('.same-class').forEach((element) => {
element.addEventListener('click', handleClick);
});
Select every matching element
A class selector starts with a period. querySelector() returns only the first matching descendant, while querySelectorAll() returns all current descendants that match the CSS selector. The result is a NodeList, so register the listener for each item rather than calling addEventListener() on the collection itself.
As an Amazon Associate I earn from qualifying purchases.
const buttons = document.querySelectorAll('.save-button');
function save(event) {
event.currentTarget.classList.add('saved');
}
buttons.forEach((button) => {
button.addEventListener('click', save);
});
addEventListener() sets up a callback on an individual EventTarget. It has been broadly available across browsers since July 2015.
Recommended Free Tools
Identify the element that was clicked
Use currentTarget for direct listeners
With a listener attached directly to each matching element, event.currentTarget is the element running the handler. In a normal function, this also refers to that element, but currentTarget remains predictable when the handler is an arrow function.
#1 Best Overall
document.querySelectorAll('.item').forEach((item) => {
item.addEventListener('click', (event) => {
console.log(event.currentTarget.textContent);
});
});
Use target when you need the original click target
event.target is the deepest element where the event began. If a button contains an icon, the target may be the icon, whereas currentTarget is the button with the listener.
document.querySelectorAll('.action-button').forEach((button) => {
button.addEventListener('click', (event) => {
console.log('button:', event.currentTarget);
console.log('original target:', event.target);
});
});
Choose direct listeners or event delegation
| Approach | Best fit | How matching works | Teardown |
|---|---|---|---|
| Direct listeners | Elements already present and independently handled | Each element receives its own listener | Remove the listener from each element using the same callback reference |
| Delegation | Items added or removed after setup, or a large changing list | One stable ancestor receives the event and filters a matching descendant | Remove one listener from the ancestor using its callback reference |
Direct listeners for existing markup
Direct registration is easy to read and lets the handler use event.currentTarget without additional filtering. It does not automatically cover elements inserted after the query runs.
Rank #2
Delegation for dynamic content
DOM events propagate through ancestors. Attach one listener to a stable container, find the nearest matching descendant, and reject matches outside that container.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →const list = document.querySelector('.list');
function save(button) {
button.classList.add('saved');
}
list.addEventListener('click', (event) => {
const button = event.target.closest('.save-button');
if (!button || !list.contains(button)) return;
save(button);
});
The containment check matters when nested components could contain another element with the same class. It ensures the handler acts only on a button belonging to this list.
Delegation and nested markup
Because a click on an icon bubbles to the list, closest('.save-button') recovers the intended button even when the original event.target is a child element.
Handle elements created later
The NodeList returned by querySelectorAll() is static. Adding a new matching element later does not add a listener to it.
Rank #4
- Run the query again after inserting new elements and register the handler on the new results.
- Attach the listener during the code that creates each element.
- Delegate from a stable ancestor when the collection changes frequently.
function addSaveButton(id) {
const button = document.createElement('button');
button.className = 'save-button';
button.dataset.id = id;
button.addEventListener('click', save);
document.querySelector('.list').append(button);
}
Remove listeners safely
Keep a named handler when a component must be destroyed. removeEventListener() requires the same function object used for registration, together with compatible capture options.
function handleClick(event) {
console.log(event.currentTarget.dataset.id);
}
const button = document.querySelector('.save-button');
button.addEventListener('click', handleClick);
// Later, during teardown:
button.removeEventListener('click', handleClick);
An inline function created during removal is a different object and will not unregister the original listener.
Best Value
Avoid duplicate and unexpected handlers
Prevent duplicate registration
If a rendering function runs repeatedly and attaches listeners each time, one click can invoke the handler multiple times. Centralize setup, mark initialized components, or remove the previous listener before re-registering it.
Inspect propagation
If a parent handler runs unexpectedly, check which elements receive the event and whether code calls stopPropagation() or stopImmediatePropagation(). These methods change how the event continues through the DOM and among listeners.
Check the selector and collection type
- Use
.same-class, including the leading period. - Use
querySelectorAll()when more than the first match is required. - Iterate the returned
NodeListbefore callingaddEventListener(). - Use delegation or a fresh query for elements added after initial setup.
Practical decision guide
- Are all matching elements present when setup runs? Use
querySelectorAll()andforEach(). - Will matching elements be inserted later? Delegate from a container that already exists, or attach the listener as each element is created.
- Does the handler need the element with the listener? Read
event.currentTarget. - Can a click originate inside nested markup? Use
event.target.closest()under delegation. - Will the component be removed or re-rendered? Preserve the handler reference and plan teardown to avoid leaks or duplicate callbacks.
The Bottom Line
For current elements, select the class with querySelectorAll(), iterate the NodeList, and register one listener per element. For changing content, put one listener on a stable ancestor and use closest() plus a containment check to identify the matching descendant.
Quick 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.




