The E7 ticketing platform had been in production for two months. Checkout completion rate: 78% across all mobile devices. Good, not great, but in line with industry benchmarks.
Then I segmented by device. iPhone 13/14/15: 81%. Pixel series: 79%. Samsung Galaxy S series: 77%. iPhone SE (2nd and 3rd gen): 34%.
Thirty-four percent. Half the completion rate of every other device. On a device that 12% of our mobile users carried.
Finding it
The data told me what but not where. The checkout funnel had 4 steps: seat selection → cart review → payment details → confirmation. I needed to know which step was bleeding.
Funnel breakdown for iPhone SE:
| Step | Drop-off (iPhone SE) | Drop-off (all mobile) |
|---|---|---|
| Seat selection → Cart review | 8% | 6% |
| Cart review → Payment details | 14% | 5% |
| Payment details → Confirmation | 44% | 11% |
| Total abandonment | 66% | 22% |
The catastrophe was at step 3 → 4. 44% of iPhone SE users who reached the payment form never completed payment. On every other device, it was 11%.
Reproducing it
I grabbed my testing iPhone SE (3rd gen, 4.7-inch screen, iOS 17). Opened the checkout. Selected seats. Reviewed cart. Tapped "Proceed to payment."
The payment form loaded. Stripe Elements rendered the card fields. Everything looked fine.
Then I tried to tap "Pay €45.00."
The button was there. I could see it. But when I tapped it, nothing happened. I tapped again. Nothing. I scrolled down — the button scrolled up. I scrolled up — the button scrolled down. It was dancing away from my thumb.
Then I noticed: Safari's address bar was in its expanded state. On a 4.7-inch screen, that steals about 50px of viewport height. The payment button was in a position: fixed footer that was positioned at bottom: 0 — but Safari's expanded address bar pushed the visual viewport up, and the button was rendered behind Safari's bottom toolbar.
The three-bug pileup
This wasn't one bug. It was three, stacking on each other:
Bug 1: The fixed footer and Safari's dynamic viewport
The payment button lived in a fixed footer:
.checkout-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
}
On large screens, this works. On iPhone SE with Safari's address bar expanded, bottom: 0 means "bottom of the layout viewport" — which is behind Safari's toolbar. The button renders but is untappable because the browser's UI is covering it.
Fix: Replace bottom: 0 with bottom: env(safe-area-inset-bottom) and switch from 100vh to 100dvh (dynamic viewport height) for the page container. dvh accounts for Safari's dynamic toolbar.
.checkout-footer {
position: fixed;
bottom: env(safe-area-inset-bottom, 0px);
left: 0;
right: 0;
padding: 16px;
}
.checkout-page {
min-height: 100dvh;
}
Bug 2: The touch target was too small
Even after fixing the viewport issue, the button was hard to tap on a 4-inch screen. The "Pay" button was 36px tall — below Apple's recommended 44px minimum touch target. On a 14 Pro, 36px is fine because the screen is big enough to aim. On an SE, every pixel of touch target matters.
Fix: Increased button height to 48px and added min-height: 44px to all interactive elements in the checkout flow.
Bug 3: Stripe Elements rendered off-screen
The Stripe card input fields (card number, expiry, CVC) are iframes. On a small screen, the three fields were stacked vertically, pushing the total form height beyond the viewport. The user had to scroll to see the CVC field, but the fixed footer covered the bottom of the scrollable area, so the CVC field was partially hidden behind the "Pay" button.
Users were entering card number and expiry, then couldn't see the CVC field, then couldn't find the submit button, then gave up.
Fix: Changed the Stripe Elements layout from stacked to inline (card number on one row, expiry + CVC on a second row). This reduced the form height enough that everything fits in the viewport on a 4.7-inch screen without scrolling behind the fixed footer.
Why nobody caught it
-
Nobody in the office had an iPhone SE. Our testing devices were an iPhone 14, a Pixel 7, and a Samsung A54. All large screens. The SE form factor is 4.7 inches — significantly smaller than modern phones.
-
Safari DevTools doesn't simulate the dynamic toolbar. When you use Safari's responsive design mode on a Mac, it renders the page at the correct screen dimensions but doesn't simulate the expanding/collapsing address bar. The bug is invisible in the simulator.
-
The drop-off looked like user behavior, not a bug. 34% completion looks like "these users aren't ready to buy," not "these users can't physically tap the button." Without device segmentation in the analytics, this would have been invisible.
The fix in production
Three changes, deployed in one PR:
100vh→100dvhon the checkout page containerbottom: 0→bottom: env(safe-area-inset-bottom)on the fixed footer- Stripe Elements layout from stacked to inline on screens under 430px wide
- All touch targets in the checkout flow set to
min-height: 44px
Checkout completion on iPhone SE went from 34% to 76% in the first week. Still below the 81% on larger iPhones — small screens will always have slightly lower completion — but the 44% → 11% drop-off at the payment step matched every other device.
What I check now
This bug changed my pre-launch QA checklist. For every checkout or form-heavy flow, I now test:
- iPhone SE (4.7-inch) with Safari's address bar expanded. Not the simulator — a real device with a real finger.
dvhinstead ofvhfor any full-height layout on mobile. Safari's dynamic toolbar makes100vhunreliable.env(safe-area-inset-bottom)on every fixed-position element. Notched phones and Safari's toolbar both eat into the bottom.- Touch targets at 44px minimum. Not 36px, not 40px. 44px. Apple's HIG exists for a reason.
- Stripe Elements layout on narrow screens. The stacked layout is the default and it breaks on small viewports. Always test with the narrowest device you support.
The iPhone SE has a 12% share of our mobile users. That's not a niche — that's thousands of people per month who couldn't buy tickets because a button was 8px too small and 50px too low.
If your conversion data shows a device-specific drop-off and you're not sure why — let's debug it together. Sometimes it's one CSS property.