"Everything Bubbles Up"
The store front kept producing small surprises this week, mostly the kind that only show up when you start clicking things.
Week of July 13, 2026
The Hard Part
I had a button with an SVG cart icon inside it. Straightforward enough — icon inside a clickable element, event listener on the button. Clicked it. The handler fired. Went to pull the data attributes off event.target and they weren't there. The target was the SVG, not the button.
This is event bubbling. A click fires on whichever element the cursor actually touches, then propagates upward through the DOM tree. The listener was on the button, but clicking the icon meant target pointed at the SVG child. I knew abstractly that events bubble — I just hadn't run into this specific wall before because I'd apparently never used target on a composite element like this. The fix is .closest('.cart-button'): instead of grabbing the element that was clicked, you walk up the tree to the nearest ancestor matching your selector. It gets you what you actually want regardless of which child triggered the event.
Worth auditing whether the same target-vs-parent mismatch exists elsewhere in the codebase.
Progress
- Add-to-cart button split into two distinct paths: single-price items get a plain button with the price, multi-price items (like subscriptions) get a select/button hybrid — dropdown for the price tier, cart SVG on the right to confirm
- Store item list rendering with the button container sizing dynamically based on which path the item takes
- Cart icon added to the header — required pulling cart/store data into the index routing so the header has what it needs at load time
- All header buttons converted from anchor tags to actual
<button>elements; all icons now loaded fromicons.phpinstead of being inlined - Logo centering on the header fixed using CSS grid — grid makes it clean to keep the logo centered relative to the viewport whether or not the cart button is present alongside it
- Mobile logo height fixed:
width: 100%wasn't computing correctly and was rendering at full default size; switched to a fixed height
Decision Made
Cart routing logic lives in the index router, not the button's click handler. The cart button has two behaviors depending on whether the user has an active cart: go to the cart page, or go to the store. My first instinct was to handle that branching inside the event handler. But the index router already handles all requests to the cart regardless of where they originate — putting the logic there means it's handled in one place rather than split across the UI and the router.
Learnings
event.target is the element you actually clicked, not the one with the listener. When you have composite elements — an icon inside a button, a label inside a list item — target will point at the innermost child the cursor touched. .closest(selector) traverses up from wherever you landed to find the right ancestor. That's the correct tool when you need data attributes or handlers attached to an outer container.
The Question That Appeared
The mobile logo fix used a hard-coded height, which works but feels like a workaround. The width: 100% approach should have been right but the calculation was off — still not totally clear why. Good enough for now, but worth revisiting when the CSS gets a proper pass.
Next Week
Test the cart routing logic with an actual saved cart and verify the badge count updates correctly when items are added. Then the cart page itself.
