The Numbers Don't Lie
Before diving into code, let's ground this in data. Mobile commerce has been the majority of e-commerce traffic for years, but conversion rates on mobile still lag behind desktop. The gap isn't about intent — it's about friction. And most of that friction lives in the checkout form.
On a project last year, we redesigned a checkout form using the patterns in this article. The mobile conversion rate went from 1.8% to 2.9% — a 61% relative improvement. No backend changes, no pricing changes. Just better HTML and CSS.
Mobile-First: Start With the Smallest Screen
The biggest mistake I see teams make is designing checkout for desktop first, then trying to squeeze it onto mobile. You end up with a two-column layout that collapses awkwardly, side-by-side fields that become unreadable, and a pay button that sits below the fold.
Flip it. Start with a single-column, stacked layout for 320px screens. Then progressively enhance for wider viewports.
Mobile (320-767px)
Desktop (768px+)
On mobile, everything stacks vertically. The order summary sits above the form as a compact bar showing just the total. On desktop, the summary gets its own column on the right. This is the layout that consistently tests best.
/* Mobile-first: single column, full-width fields */
.checkout-form {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1.5rem;
}
.checkout-field {
width: 100%;
min-height: 48px;
padding: 0.75rem 1rem;
font-size: 1rem; /* prevents iOS auto-zoom */
border: 1.5px solid #d4d9d6;
border-radius: 6px;
}
/* At 480px: expiry and CVV go side by side */
@media (min-width: 480px) {
.checkout-row-split {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
}
/* At 768px: form + order summary side by side */
@media (min-width: 768px) {
.checkout-layout {
display: grid;
grid-template-columns: 1fr 360px;
gap: 2.5rem;
align-items: start;
}
}
Why font-size: 1rem matters: iOS Safari auto-zooms the viewport when a user taps an input with a font size below 16px. On a checkout form, this is jarring — the page zooms in, the user loses context, and they have to pinch back out. Setting font-size: 1rem on all inputs is the single easiest conversion win on mobile.
Touch Targets: The 48px Rule
WCAG 2.5.8 specifies a minimum target size of 24x24 CSS pixels, but Google's Material Design guidelines and Apple's HIG both recommend 48px — and on a checkout form, I'd argue 48px is the floor, not the ceiling. When someone is entering their credit card number on a bus, you want generous tap areas.
This applies to more than just buttons:
- Text inputs —
min-height: 48pxwith adequate padding - Radio buttons and checkboxes — wrap the entire label in the clickable area, not just the tiny circle
- Payment method selectors — each option should be a full-width tappable row, not a small icon
- The "Edit" and "Back" links — these are often 12px text with no padding, nearly impossible to tap accurately
/* Touch-friendly payment method selector */
.payment-method-option {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.875rem 1rem;
min-height: 56px;
border: 1.5px solid var(--color-border);
border-radius: 8px;
cursor: pointer;
transition: border-color 0.15s ease;
}
.payment-method-option:hover,
.payment-method-option:focus-within {
border-color: var(--color-accent);
}
/* Hide native radio, style the whole row */
.payment-method-option input[type="radio"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.payment-method-option input[type="radio"]:checked + label {
border-color: var(--color-accent);
background: rgba(13, 148, 136, 0.04);
}
Input Types and Autofill — Free Conversion Gains
This is the part that costs zero design effort but has an outsized impact. Using the right input type and autocomplete attributes means the browser does half the work for the user. On mobile, it means the right keyboard appears. With autofill, it means the user's saved card details populate in one tap.
Here's the exact attribute combination I use on every checkout form:
<!-- Card number: numeric keyboard + browser autofill -->
<input
type="text"
inputmode="numeric"
pattern="[0-9\s]*"
autocomplete="cc-number"
placeholder="1234 5678 9012 3456"
aria-label="Card number"
>
<!-- Expiry: month/year autofill -->
<input
type="text"
inputmode="numeric"
autocomplete="cc-exp"
placeholder="MM / YY"
aria-label="Expiration date"
>
<!-- CVV: numeric + no autofill (security) -->
<input
type="text"
inputmode="numeric"
pattern="[0-9]*"
autocomplete="cc-csc"
maxlength="4"
placeholder="CVV"
aria-label="Security code"
>
<!-- Cardholder name -->
<input
type="text"
autocomplete="cc-name"
placeholder="Name on card"
aria-label="Cardholder name"
>
A few things to note: use inputmode="numeric" instead of type="number" for card fields. type="number" adds increment/decrement arrows, strips leading zeros, and behaves inconsistently across browsers. inputmode="numeric" gives you the numeric keyboard without any of that baggage.
Don't disable autofill. I've seen teams add autocomplete="off" to card fields for "security." This doesn't improve security — the card data is already stored in the user's browser. All it does is force users to manually type 16 digits on a phone keyboard, which tanks your conversion rate.
Single-Page vs. Multi-Step Checkout
This is one of the most debated topics in checkout UX, and the answer depends on how many fields you're collecting. My rule of thumb: if you have fewer than 8 fields, use a single page. If you need shipping address, billing address, delivery options, and payment — break it into steps.
The key to multi-step checkout on mobile is a clear progress indicator and the ability to go back and edit previous steps without losing data. I use a simple step counter ("Step 2 of 4") rather than a full progress bar — it takes less horizontal space and is easier to read on small screens.
For the step transitions, avoid full page reloads. Use CSS transitions on a container that slides or fades between steps. Keep the order summary visible (or at least the total) at every step so the user always knows what they're paying.
/* Multi-step form container */
.checkout-steps {
overflow: hidden;
position: relative;
}
.checkout-step {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.checkout-step[hidden] {
display: block !important;
position: absolute;
top: 0;
left: 0;
width: 100%;
opacity: 0;
pointer-events: none;
transform: translateX(30px);
}
/* Step indicator */
.step-indicator {
font-size: 0.8125rem;
color: var(--color-text-tertiary);
font-weight: 500;
padding: 1rem 0;
}
Payment Method Layout
When you offer multiple payment methods — card, Apple Pay, Google Pay, PayPal — the layout matters more than you'd think. The pattern that converts best in my experience: put the express payment options (Apple Pay, Google Pay) at the top as prominent buttons, then show the card form below with a subtle "Or pay with card" divider.
On mobile, stack everything vertically. On desktop, you can show express options side by side. But never make the user choose a tab or radio button before seeing the card form — the card form should be visible by default, with express options as shortcuts above it.
/* Express payment buttons — stacked on mobile, row on desktop */
.express-payments {
display: flex;
flex-direction: column;
gap: 0.625rem;
margin-bottom: 1.5rem;
}
@media (min-width: 480px) {
.express-payments {
flex-direction: row;
}
.express-payments .express-btn {
flex: 1;
}
}
.express-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-height: 48px;
padding: 0.75rem 1rem;
border: 1.5px solid var(--color-border);
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
background: #fff;
cursor: pointer;
}
/* "Or pay with card" divider */
.payment-divider {
display: flex;
align-items: center;
gap: 1rem;
margin: 1.5rem 0;
font-size: 0.8125rem;
color: var(--color-text-tertiary);
}
.payment-divider::before,
.payment-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--color-border);
}
Autofill Optimization — The Details That Matter
Browser autofill can fill an entire checkout form in under 2 seconds. But it only works if your HTML cooperates. Beyond the autocomplete attributes on inputs, there are structural things that help:
- Use a single
<form>element — don't split shipping and billing into separate forms. Autofill works best when it can see all the fields at once. - Use
<label>elements — browsers use label text to identify fields. A label that says "Card Number" helps the browser match the right autofill value. - Don't dynamically inject fields — if the card number input is rendered after a JavaScript framework mounts, some browsers miss it during autofill detection. Render the fields in the initial HTML when possible.
- Test with real autofill — Chrome, Safari, and Firefox all handle autofill slightly differently. Test on all three with saved card data.
Quick win: Add autocomplete="country" to your country selector and autocomplete="postal-code" to the zip/postal field. When the browser fills the address, it'll also select the right country and fill the postal code. This alone saves 15-20 seconds of manual input on mobile.
What Actually Moves the Needle
After building and optimizing checkout forms across multiple projects, these are the changes that consistently produce measurable conversion improvements:
- Mobile-first layout. Design for 320px, enhance upward. Single column on mobile, two columns on desktop.
- Correct input attributes.
inputmode="numeric", properautocompletevalues, visible labels. Free performance. - 48px touch targets everywhere. Inputs, buttons, radio options, back links — everything the user taps.
- Express payments above the fold. Apple Pay and Google Pay at the top, card form below. Don't make users hunt.
- Never hide the total. The amount should be visible at every step of checkout, on every screen size.
- Test on real devices. Simulators miss touch target issues, keyboard overlap, and autofill quirks. Test on a real phone.
References
- web.dev — Payment and Address Form Best Practices
- MDN — HTML autocomplete Attribute
- MDN — inputmode Attribute
- Baymard Institute — Mobile Checkout Usability
- Baymard Institute — Cart Abandonment Rate Statistics
- W3C — WCAG 2.2 Target Size (Minimum)
- web.dev — Sign-in Form Best Practices
Disclaimer: This article reflects the author's personal experience and opinions. Conversion rate figures are based on specific projects and may not generalize to all contexts. Always A/B test changes on your own checkout flow. Product names and trademarks are property of their respective owners.