store: sticky buy bar makes two footer links untappable on mobile PDP #89

Closed
opened 2026-09-05 08:51:29 +00:00 by yuris · 0 comments
Owner

Found while reviewing #87 (spacing audit). Pre-existing on main, reproduces identically in the approved mockup design/produk-shockbreaker-avanza.html, so it is not a regression and was deliberately left out of #87's scope.

Symptom

On the mobile product detail page at max scroll, two footer links cannot be tapped. The sticky buy bar covers them.

Measured at 375x812 on the built static export:

.sticky-buy height 69px, position: fixed; inset: auto 0 0 0
bar top edge y 743
body padding-bottom 0px
.pdp padding-bottom 96px

Four footer runs fall under the bar, and document.elementFromPoint at each link's own centre returns the bar instead of the link:

footer link href resolves to
Oli & filter /products?kategori=oli itself (partially covered)
Kaki-kaki /products?kategori=kaki-kaki SPAN.price (the bar)
Jam buka & lokasi /contact itself (partially covered)
Cara pesan /contact#cara-pesan BUTTON.btn (the bar)

So Kaki-kaki and Cara pesan are unreachable by tap on this route at this width. Roughly 21px of footer content is permanently obscured.

Why the existing padding does not cover it

.pdp { padding-bottom: 96px } inside the max-width: 700px query looks like clearance for the bar, and that was my own reading in #87 before measuring. It is wrong twice:

  1. Over-reserved. The bar is 69px; 96px reserves 27px more than needed (+39%).
  2. On the wrong element. The bar is position: fixed, so it overlays the bottom of the viewport, not the end of .pdp. At max scroll .pdp is already off-screen — its bottom edge sits at viewport y -11 — so the reserve protects a box that is nowhere near the bar. What is actually under the bar at that scroll position is the footer, which reserves nothing.

Suggested fix

Reserve on the element that is actually overlaid. Since the bar is viewport-fixed, the scroll container is what needs the room:

  • body { padding-bottom: 69px } (or the bar's measured height) inside the same max-width: 700px query, on the routes where the bar is displayed; and
  • drop .pdp's 96px to whatever the layout wants for its own rhythm, independent of the bar.

Worth deriving the value from the bar rather than hardcoding a second magic number — the 96px/69px mismatch is exactly the drift that happens when two places encode the same physical measurement.

Scope note

.pdp { padding-bottom: 96px } is generated into app/globals.css by scripts/port-design.mjs from design/assets/tokens.css, and the mockup has the same defect. So fixing it properly means correcting the design source and re-vendoring, the same route #87 took for the layanan spacing — not hand-editing globals.css, which the port would overwrite.

Verification for whoever takes it

Build the static export, serve it, then at 375x812 on /products/<slug> scroll to document.documentElement.scrollHeight and assert document.elementFromPoint(cx, cy) returns the anchor itself for every footer a. That check fails today and should pass after.

Found while reviewing #87 (spacing audit). Pre-existing on `main`, reproduces identically in the approved mockup `design/produk-shockbreaker-avanza.html`, so it is not a regression and was deliberately left out of #87's scope. ## Symptom On the mobile product detail page at max scroll, two footer links cannot be tapped. The sticky buy bar covers them. Measured at 375x812 on the built static export: | | | |---|---| | `.sticky-buy` height | **69px**, `position: fixed; inset: auto 0 0 0` | | bar top edge | y **743** | | `body` padding-bottom | **0px** | | `.pdp` padding-bottom | 96px | Four footer runs fall under the bar, and `document.elementFromPoint` at each link's own centre returns the bar instead of the link: | footer link | href | resolves to | |---|---|---| | `Oli & filter` | `/products?kategori=oli` | itself (partially covered) | | **`Kaki-kaki`** | `/products?kategori=kaki-kaki` | **`SPAN.price`** (the bar) | | `Jam buka & lokasi` | `/contact` | itself (partially covered) | | **`Cara pesan`** | `/contact#cara-pesan` | **`BUTTON.btn`** (the bar) | So `Kaki-kaki` and `Cara pesan` are unreachable by tap on this route at this width. Roughly 21px of footer content is permanently obscured. ## Why the existing padding does not cover it `.pdp { padding-bottom: 96px }` inside the `max-width: 700px` query looks like clearance for the bar, and that was my own reading in #87 before measuring. It is wrong twice: 1. **Over-reserved.** The bar is 69px; 96px reserves 27px more than needed (+39%). 2. **On the wrong element.** The bar is `position: fixed`, so it overlays the bottom of the *viewport*, not the end of `.pdp`. At max scroll `.pdp` is already off-screen — its bottom edge sits at viewport y **-11** — so the reserve protects a box that is nowhere near the bar. What is actually under the bar at that scroll position is the footer, which reserves nothing. ## Suggested fix Reserve on the element that is actually overlaid. Since the bar is viewport-fixed, the scroll container is what needs the room: - `body { padding-bottom: 69px }` (or the bar's measured height) inside the same `max-width: 700px` query, on the routes where the bar is displayed; and - drop `.pdp`'s 96px to whatever the layout wants for its own rhythm, independent of the bar. Worth deriving the value from the bar rather than hardcoding a second magic number — the 96px/69px mismatch is exactly the drift that happens when two places encode the same physical measurement. ## Scope note `.pdp { padding-bottom: 96px }` is generated into `app/globals.css` by `scripts/port-design.mjs` from `design/assets/tokens.css`, and the mockup has the same defect. So fixing it properly means correcting the design source and re-vendoring, the same route #87 took for the layanan spacing — not hand-editing `globals.css`, which the port would overwrite. ## Verification for whoever takes it Build the static export, serve it, then at 375x812 on `/products/<slug>` scroll to `document.documentElement.scrollHeight` and assert `document.elementFromPoint(cx, cy)` returns the anchor itself for every `footer a`. That check fails today and should pass after.
yuris closed this issue 2026-09-28 20:43:30 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/garage#89
No description provided.