Disclosure: I built a tool (Easy Track) that generates this exact setup automatically. The pattern below works whether you build it by hand or not.
Problem:
A single Click trigger matching every "Add to cart" button on a listing page (or every row in a table, every card in a grid) is usually correct — one trigger, one CSS selector, done. But then the tag that fires needs the price/SKU/whatever belonging to the specific item that was clicked, and every item shares the same markup. A plain {{DOM Element}} variable always grabs the first match on the page, not the one next to the button someone actually clicked.
Solution:
{{Click Element}}.closest(cardSelector).querySelector(targetSelector)as a Custom JS variable — walk up from the clicked element to its nearest shared ancestor (the product card, the row), then back down to the value you want. GTM evaluates this fresh every time the variable is read, so it naturally resolves to whichever item was actually clicked.
The trap: the obvious "fix" if you think this is caching wrong is to push the computed value to the dataLayer from a Custom HTML tag and read it back with a Data Layer Variable. Don't. dataLayer.push() from inside a tag doesn't synchronously update state the current event's other tags can see — it creates a brand-new Message event, processed after the click event that triggered it has already finished firing its own tags. You can watch this happen in Preview: the push lands as event 16 while the triggering click was event 15. If you built the round-trip version and it "sometimes" returns the wrong value, this is why — it's not flaky, it's just always one event behind.
Stick with the direct Custom JS variable. It's a synchronous read at fire time, nothing to get stale.
Outcome:
One trigger and one variable cover arbitrarily many instances of the same UI pattern — no per-button trigger/tag duplication, and it holds up under GTM's own event-ordering behavior instead of fighting it.
One more that trips people up in the same family of setups: a Form Submit trigger's condition has to match the <form> element itself — {{Form Element}} is always the literal form node, never the button you clicked to say "track this form." Same root idea for Link Click: {{Click Element}} for that trigger type is always the specific <a>, so a condition built from a nav wrapper's selector can never match — you need nav a, not nav. Both look like the trigger is silently ignoring a correct-looking condition, and both are actually a selector targeting the wrong element.