MODRACXKENNETH D'SILVA

← Archive & Insights

Designing for Conversions: UX Architecture & Frictionless Checkout

A client-side postcode regex rejected lowercase input for sixteen weeks and cost about £180,000. Checkout is where small decisions get expensive.

By Kenneth D'SilvaReading Time: 29 min readCategory: UX & Design

1. The Postcode Field That Was Quietly Rejecting Scotland

In February 2023 a footwear retailer asked me to look at their checkout because conversion had drifted down about 8% over a quarter and nobody could point at a cause. No deploy correlated with it, no traffic mix change, no price change.

What had happened was a regex. Someone had added client-side postcode validation four months earlier to cut down on failed deliveries. The pattern was case-sensitive, anchored at both ends, and required exactly one space in the middle. So SW1A 1AA passed and sw1a 1aa did not. Neither did SW1A1AA. Neither did anything with a trailing space, which is what you get if you paste a postcode from an email or if an Android keyboard adds one after autocomplete. And the outward-code portion was written from a sample of English examples, so a handful of Scottish and Northern Irish formats — IV27, KW1, the single-digit Belfast codes — failed outright regardless of how they were typed.

The field went red with the message "Please enter a valid postcode." The customer, looking at their own postcode which they have typed correctly their entire life, tried it three or four times and left. There was no server-side log of it because the form never submitted. The only trace was in the client-side analytics, in a field-level error event that nobody had built a report on.

Roughly 2,400 sessions hit it over sixteen weeks. At their AOV and a conservative estimate of how many would have completed, it cost somewhere near £180,000 in revenue to save maybe forty failed deliveries.

I have shipped a version of that bug myself. On a jewellery client in 2019 I added a phone number pattern that required 11 digits and quietly rejected every customer who typed spaces. It ran for nine days. I found it because a colleague tried to place a test order on their phone and swore at their desk.

The checkout is the part of a storefront where every small decision is measurable and most of them are made by nobody in particular. This piece is about that half of the job: form design and field reduction, guest checkout, when to validate and how to phrase failure, the trust signals that do something versus the ones that are decoration, and why most of the A/B tests people run on all this cannot possibly detect the effects they claim to have found. The browse and discovery half — taxonomy, facets, search, product page hierarchy — is covered separately in the piece on designing the discovery experience.

2. Where Checkouts Actually Lose People

Aggregate cart abandonment numbers get quoted at around 70%, and the figure is close to useless because it includes everyone who added something to a cart as a bookmark and never intended to buy.

The number worth measuring is the step-to-step drop within the checkout itself, once someone has started it. Across the stores I have instrumented, a checkout in reasonable shape loses something like this: 8 to 14% between cart and the first checkout step, 10 to 20% on the address step, 5 to 12% on delivery selection, and 8 to 18% at payment. Multiply those and a decent checkout completes around 55 to 65% of the people who start it. A bad one completes 30%.

The address step is consistently the largest loss and it gets the least attention, because it is not glamorous and there is nothing to argue about in a design review. Payment gets endless attention because it is where the money visibly is, and by the time someone reaches payment they have already invested enough effort that they mostly finish.

So the priority order is roughly inverted from where most teams spend their time. Fix the address step first.

// Minimal step instrumentation. Fire on entry to each step and on the
// first interaction within it. The gap between the two is where people
// stall, and it is invisible if you only track step completions.
const steps = ['cart', 'contact', 'address', 'delivery', 'payment', 'review'];

function trackStep(step) {
  const entered = performance.now();
  let interacted = false;

  const onFirstInput = () => {
    if (interacted) return;
    interacted = true;
    send('checkout_step_engaged', {
      step,
      ms_to_first_input: Math.round(performance.now() - entered),
    });
  };
  document.querySelector(`[data-step="${step}"]`)
    .addEventListener('input', onFirstInput, { once: true });

  // Abandonment is the absence of an event, so record entry unconditionally
  // and reconcile server-side. Beacon survives the tab closing.
  navigator.sendBeacon('/collect', JSON.stringify({
    event: 'checkout_step_entered', step, ts: Date.now(),
  }));
}

3. Guest Checkout Is The Default, Not An Option

Forced account creation is the single most reliably damaging thing you can do to a checkout, and it is still on maybe a fifth of the stores I audit, usually because a marketing lead wants the database.

The argument against it is not that accounts are bad. It is that you are asking someone to make a decision about a long-term relationship with your brand at the exact moment they are trying to complete a transaction, and you are charging them a password for the privilege.

What I build instead: guest checkout as the default path, no account decision presented before payment, and an offer to create an account on the confirmation page where the only thing missing is a password because you already have everything else. Take-up on that post-purchase offer, in the four stores where I have measured it, has run between 22% and 38%. That is lower than a forced signup rate of 100%, obviously, and the accounts you get are from people who actually wanted one.

There is a middle path worth knowing about. Shopify's checkout has effectively made this decision for you and it is one of the honest arguments for the platform. Magento requires configuration and, in my experience, the removal of an extension somebody installed in 2018.

The related mistake is the login prompt that appears when a guest enters an email address matching an existing account. "An account already exists with this email, please log in." Now the customer has to remember a password they set two years ago, and the reset flow takes them to their inbox, and about a third of them do not come back. Let them check out as a guest and attach the order to the existing account server-side afterwards. The only case where I would block that is if the account has stored payment methods or loyalty balance, where you have a genuine account-takeover concern.

4. Count The Fields. Then Delete Some.

Every field is a small tax and the tax compounds. I count fields as the first measurement on any checkout audit, and the number is nearly always higher than the team's estimate.

The footwear retailer's checkout, before I touched it, presented 23 inputs across three steps for a logged-out guest buying one pair of shoes to a UK address. Twenty-three. That included title, company name, a second address line, a county field, a "how did you hear about us" dropdown, and two separate marketing consent checkboxes.

The reduced version was eleven, and one of those was optional.

FieldVerdictReasoning
Title (Mr/Mrs/Ms)DeleteUsed in nothing downstream except a mail merge nobody runs. Also a needless gender question.
First / last nameKeep, as two fieldsOne combined field is tidier but breaks against carriers that require them separately. I have lost this argument to reality twice.
Company nameConditionalShow on B2B, hide on B2C, never show unconditionally.
Address line 2ProgressiveHidden behind "add apartment, suite" — present for the people who need it, absent for the 80% who do not.
County / stateDelete in the UK, keep in the USUK deliveries do not need it. Royal Mail has not needed it since postcodes existed.
Phone numberKeep, optional, labelled why"For delivery updates only" — carriers genuinely use it. Required only for courier services that mandate it.
Confirm emailDeleteCopy-paste defeats it. Better: a typo-detection library that suggests "did you mean gmail.com".
Confirm passwordDeleteA show-password toggle solves the same problem with one field instead of two.
Billing addressDefault to shippingCheckbox, checked. Around 85% of orders match.
"How did you hear about us"Move post-purchaseIt is market research on the payment page. Ask on the thank-you page where it costs nothing.
Marketing consentOne checkbox, uncheckedTwo separate ones is a dark pattern and a UK GDPR problem.

Address lookup by postcode changes this arithmetic entirely and is the highest-value single change available on a UK checkout. Postcode plus house number, one lookup, four fields populated. Loqate, Ideal Postcodes and Fetchify all do it for somewhere between 2p and 5p per lookup, which against an order value of £60 is not a discussion.

Two implementation rules I insist on. There must always be a visible "enter address manually" escape, because lookup services miss new-build addresses and every property in a handful of postcodes, and a customer who cannot proceed is worth more than the tidy design. And the populated fields must remain editable, because the lookup gets flat numbers and building names wrong often enough to matter.

<!-- The manual escape is not a fallback for JS failure. It is a
     first-class path, because address data is genuinely incomplete. -->
<div class="address-lookup">
  <label for="pc-lookup">Postcode</label>
  <input id="pc-lookup" name="postcode_lookup" type="text"
         autocomplete="postal-code" inputmode="text"
         aria-describedby="pc-help" />
  <p id="pc-help">We'll find your address. You can edit it after.</p>
  <button type="button" data-action="lookup">Find address</button>
  <button type="button" data-action="manual" class="link-button">
    Enter address manually
  </button>
</div>

5. The Attributes That Do More Than Any Redesign

Before touching layout, get the input semantics right. This is thirty minutes of work and it changes the mobile experience more than a fortnight of visual design.

Every field needs the correct type, the correct autocomplete token, and where they differ, the correct inputmode. Browsers and password managers have been able to fill an entire address form from a single tap since about 2017, and they only do it if the tokens are right. The tokens are specified in the HTML standard's autofill section and there are more of them than people realise.

<!-- The full set for a UK delivery address. Every token here is
     from the HTML autofill spec; browsers ignore anything else. -->
<input name="email"      type="email"  autocomplete="email"
       inputmode="email"  spellcheck="false" autocapitalize="off">

<input name="fname"      type="text"   autocomplete="given-name">
<input name="lname"      type="text"   autocomplete="family-name">

<input name="line1"      type="text"   autocomplete="address-line1">
<input name="line2"      type="text"   autocomplete="address-line2">
<input name="city"       type="text"   autocomplete="address-level2">
<input name="postcode"   type="text"   autocomplete="postal-code"
       autocapitalize="characters" spellcheck="false">
<input name="country"    autocomplete="country-name">

<!-- tel, not number: a number input strips leading zeros and shows
     spinner arrows on a phone number, which is absurd. -->
<input name="phone"      type="tel"    autocomplete="tel"
       inputmode="tel">

<!-- One-time passcodes autofill from SMS on iOS and Android
     with this token and no JavaScript at all. -->
<input name="otp"        type="text"   autocomplete="one-time-code"
       inputmode="numeric" maxlength="6">

Three specifics worth calling out because they are wrong on most sites I open.

type="number" on anything that is not a quantity. It strips leading zeros, it exposes spinner arrows, and on some Android keyboards it produces a numeric pad without a comma. Postcodes, card numbers and phone numbers are all text with an inputmode.

Missing autocapitalize="off" on the email field. iOS capitalises the first letter by default, the customer does not notice, and while email addresses are case-insensitive in the local part per most providers, your own validation or your ESP's deduplication may not be.

And a font size below 16px on any input, which makes iOS Safari zoom the page on focus, shifting the layout under the customer's thumb. It is a one-line fix and it is present on a surprising share of otherwise careful builds.

6. When To Tell Someone They Are Wrong

Validation timing is the most consequential form decision after field count, and it has a correct answer that almost nobody implements.

The three common approaches: validate on every keystroke, validate on blur, validate on submit. All three are wrong on their own.

On keystroke is hostile. The customer types k in the email field and is immediately told their email is invalid. It will be invalid for the next twenty keystrokes. Watching a form scold you while you type is genuinely unpleasant and it makes people slow down and second-guess correct input.

On submit is too late. The customer fills eleven fields, presses the button, and is bounced back to field three. On mobile, where the erroring field may be off-screen, this is where people give up.

On blur alone is close but breaks in one specific way: a customer who fixes a field still sees the error until they leave it again, so they correct the problem, look at the still-red field, and assume their correction did not work.

What actually works is a hybrid that people sometimes call reward early, punish late. Validate on blur the first time. Once a field has an error, switch that field to validating on input, so the error clears the instant it becomes valid. Never validate a field the customer has not yet touched.

// Reward early, punish late. The `touched` flag is the whole idea:
// a field that has never errored validates on blur; once it has
// errored, it validates on every keystroke so the fix is instant.
function wireValidation(field, validate) {
  let hasErrored = false;

  const run = () => {
    const message = validate(field.value);
    if (message) {
      hasErrored = true;
      showError(field, message);
    } else {
      clearError(field);
    }
  };

  field.addEventListener('blur', run);
  field.addEventListener('input', () => {
    // Silent until the field has failed once. Before that, typing
    // must never produce a red border.
    if (hasErrored) run();
  });
}

function showError(field, message) {
  field.setAttribute('aria-invalid', 'true');
  const box = document.getElementById(field.getAttribute('aria-errormessage'));
  box.textContent = message;
  field.classList.add('is-invalid');
}

function clearError(field) {
  field.removeAttribute('aria-invalid');
  document.getElementById(field.getAttribute('aria-errormessage')).textContent = '';
  field.classList.remove('is-invalid');
}

The server still validates everything, obviously. Client-side validation is a courtesy to the customer and nothing else, and treating it as a control is how you end up with an API that trusts a hidden field.

On permissiveness: be far more generous than feels correct. Strip whitespace, accept any case, accept dashes and spaces in card numbers and phone numbers, accept a postcode with or without the space. The footwear retailer's regex is the canonical warning. If you must validate a postcode, do it on the server against a real lookup, and treat a failure as a warning that lets the order through rather than a block, because a customer who knows their own address is right and cannot proceed is a lost order and an angry email.

7. Errors That Say What To Do Next

"Invalid input." "This field is required." "An error occurred." Each of these tells the customer that something is wrong and nothing about what to do, which is the entire job of an error message.

A good error names the field, states what is wrong specifically, and says what a valid value looks like. "Please enter a postcode, for example SW1A 1AA" beats "Invalid postcode" by a distance you can measure.

Placement: immediately below the field, in text, not in a tooltip and not in a summary at the top of the page. A summary at the top is an addition for screen reader users and keyboard users after a submit attempt, not a replacement for inline messages.

Colour alone fails. A red border with no text is invisible to about one in twelve men, and it is also invisible to anyone in bright sunlight on a phone. Red border plus an icon plus a text message.

The category of error people handle worst is the one that arrives from the server after submission: payment declined, item went out of stock while you were typing, delivery address unserviceable. These are frightening because the customer has just pressed a button involving money.

// Decline messages. The PSP's raw reason code is for your logs;
// the customer needs an action, and the action differs by code.
const DECLINE_COPY = {
  insufficient_funds: {
    text: 'Your bank declined the payment due to insufficient funds. ' +
          'Try another card, or check your balance and try again.',
    retrySameCard: true,
  },
  do_not_honor: {
    // The most common and least informative code. Do NOT relay it.
    text: 'Your bank declined the payment. They will not tell us why. ' +
          'The quickest fix is usually another card, or calling your bank ' +
          'to approve the transaction.',
    retrySameCard: true,
  },
  expired_card: {
    text: 'That card has expired. Please check the expiry date or use ' +
          'another card.',
    retrySameCard: false,
  },
  fraud_suspected: {
    // Never tell the customer fraud was suspected. It reads as an
    // accusation, and it tells a real fraudster what tripped.
    text: 'We could not process that payment. Please try another payment ' +
          'method, or contact us on 0800 000 0000 and we will help.',
    retrySameCard: false,
  },
};

The rules underneath that: never show a raw gateway code, never blame the customer, always offer a next action, and never clear the form. Wiping the entered card details after a decline is a small cruelty that I still see on live stores.

Stock changing mid-checkout deserves specific handling. The customer should not discover at payment that the item they are buying went out of stock, so reserve inventory at the start of checkout with a short TTL. If it genuinely fails, tell them which line failed, what remains, and give them a one-click option to proceed with the rest rather than dumping them back at the cart with an unexplained change.

8. One Page Or Several

This argument runs forever and the honest answer is that the number of steps matters far less than what is in them.

The case for a single page: no page transitions, everything visible, fewer opportunities to abandon between steps. The case for steps: less overwhelming on first sight, each step has a clear purpose, and you can capture the email address before the customer sees the delivery cost, which gives you an abandonment recovery route.

I default to an accordion — a single page with sequential sections that expand one at a time and collapse to a summary line when complete. It gets the transition-free feel of one page with the focus of steps, and the completed summary lines give the customer a visible sense of progress, which is the actual psychological benefit people attribute to multi-step flows.

What matters more than the count, in rough order:

Whether the total, including delivery, is visible at every point. A total that changes when the customer reaches the delivery step is the single most common cause of abandonment at that step.

Whether the customer can edit an earlier section without losing later input. Accordion patterns handle this well; multi-page flows frequently do not.

Whether the order summary is visible on mobile without scrolling away from the form. Collapsed by default with the total in the header is the pattern that works; a summary pinned below 400 pixels of form is a summary nobody sees.

Whether pressing back does something sensible. A checkout that loses state on back is broken regardless of how many pages it has.

9. Wallets Are The Biggest Single Win, With Caveats

Apple Pay, Google Pay, Shop Pay, PayPal Express. On mobile these collapse the entire address and payment sequence into a biometric confirmation, and the effect is larger than anything else discussed here.

The numbers I have measured on my own clients: wallet checkouts complete at between 1.6 and 2.4 times the rate of card checkouts on mobile. That is not a like-for-like comparison, because people who have a wallet configured skew towards being comfortable buying on a phone. But the direction is not in doubt and the effect survives every attempt I have made to control for it.

Where wallets cause problems, and these are real.

The address you get back may not be the address the customer wants. Apple Pay returns the card's billing address as a default shipping address, and people's cards are registered to old addresses constantly. Always show the returned address for confirmation before completing, rather than trusting it.

Email addresses from wallets are frequently the Apple relay address, which forwards but which your ESP may treat oddly and which the customer will not recognise in a "we sent your receipt to..." message.

Discount codes and wallets interact badly. If your promotion logic runs after the wallet sheet closes, the customer sees one total in the sheet and a different one on the confirmation. Compute the final total before presenting the sheet.

And the wallet button placement question: on the cart page and at the top of checkout, above the email field, not buried below the card form. A wallet button below the form is a wallet button found by the people who did not need it.

10. Trust Signals: What Works And What Is Cargo Cult

This is where I disagree with a lot of conversion advice, because a large amount of what gets sold as trust building is decoration that occupies space and occasionally does harm.

What I have seen move numbers, in order.

A visible returns policy with a specific number of days, near the buy action. "Free returns within 60 days" as a line on the product page and in the checkout summary. This is a real risk reversal and it is the most consistent positive I have measured.

A delivery date, not a delivery class. "Arrives Thursday 14 March" beats "Standard delivery 3-5 working days" every time I have tested it. It removes a calculation the customer would otherwise have to do and it commits you, which is why merchandising teams resist it.

Real reviews, with the bad ones visible. A product with 4.3 stars and some critical reviews is more credible than one with 5.0 and forty reviews. Filtering out negatives is detectable and it destroys the signal you were trying to create.

A phone number and a physical address in the footer. Almost nobody calls. That is not the point — the point is that a business willing to publish a phone number is a business you can find, and its absence is noticed subconsciously.

Recognisable payment method logos. Visa, Mastercard, PayPal, Klarna. These signal that the transaction is routed through institutions the customer already trusts.

What I regard as cargo cult.

Security badge images. The Norton, McAfee and generic padlock graphics. Baymard's testing found these have inconsistent effects and some actively reduce trust by drawing attention to security as a topic. Worse, they are usually static images with no verification behind them, and every phishing site uses them too. The one exception is a genuine, clickable trust seal from a service the customer actually recognises, and there are perhaps two of those.

"SSL secured" text. HTTPS has been the default since roughly 2018 and browsers now flag its absence rather than its presence. Announcing it is like a restaurant advertising that it washes its hands.

Live visitor counters. "17 people are viewing this item." Some of these are honest and most are a random number generator, and customers have learned to read them as such. I have refused to build these twice.

Award badges from 2019. A dated badge is worse than none.

Excessive testimonial carousels. Auto-rotating quotes attributed to "Sarah M." are read as marketing copy, correctly.

The distinction I use: a trust signal works if it transfers risk from the customer to you, or if it is independently verifiable. Free returns transfers risk. A named delivery date transfers risk. A padlock graphic does neither.

11. Urgency, Scarcity, And Where I Stop

Genuine scarcity information helps people decide. "Only 2 left in stock" when there are genuinely two left is useful and it converts, and I build it without hesitation.

Manufactured scarcity is a different thing and I will not build it. Countdown timers that reset on refresh. "Only 3 left" hardcoded in a template. Basket reservation timers on a store with no inventory pressure. These work in the short term, in the sense that they raise conversion in a two-week test, and they cost you the thing that makes a customer come back.

There is also a legal dimension that has sharpened considerably. The UK's Digital Markets, Competition and Consumers Act 2024 brought drip pricing and fake urgency claims into direct enforcement scope, with the CMA able to fine up to 10% of global turnover without going to court first. Drip pricing specifically — showing a price and adding mandatory fees later in the flow — is the one most ecommerce sites are casually guilty of. If a fee is unavoidable, it belongs in the headline price.

The practical version of my position: if a claim on your checkout would embarrass you in a screenshot, do not ship it. That test has never failed me and it is faster than reading the guidance.

12. The Cart Is Part Of The Checkout

Treating the cart as a separate concern is a mistake that shows up as a drop between cart and checkout that nobody investigates.

What belongs in the cart: line items with images large enough to identify, quantity controls that are actual buttons rather than a number input with spinners, the ability to remove with an undo rather than a confirmation dialog, the delivery cost or an estimate, the total, and one primary action.

What does not belong: cross-sell carousels above the checkout button, a discount code field given visual prominence, and any secondary action styled like the primary one.

The discount code field deserves its own note because it is the most consequential small element in the cart. A prominent empty box labelled "Discount code" tells every customer without one that a discount exists and they are not getting it. A meaningful share of them leave to search for a code, and some of those never return, and the ones who do return often arrive via an affiliate coupon site that now takes commission on an order you already had.

The fix is not to hide it, which frustrates people who have a legitimate code from your own email. Make it a small text link that expands. Better still, if the customer arrived with a code in the URL, apply it automatically and show it as applied rather than presenting an empty field at all.

// Auto-apply a code carried in the URL, then never show an empty
// promo field to that customer. The field expands from a text link
// for everyone else.
const url = new URL(location.href);
const code = url.searchParams.get('discount') || url.searchParams.get('coupon');

if (code) {
  applyDiscount(code).then((result) => {
    if (result.ok) {
      renderApplied(result.label, result.saving);
    } else {
      // A dead code from an old email must not fail silently. Say so,
      // and say what it was, or support will get the call instead.
      renderNotice(`The code ${code} has expired or is not valid for these items.`);
    }
  });
}

13. Abandonment Recovery Without Being Unpleasant

Capturing the email address early is the mechanic that makes recovery possible, and it is the strongest argument for a multi-step or accordion checkout over a single page.

What works: one email at around 45 to 90 minutes, one at 24 hours, and stop. A third message is where the complaint rate rises and where you start training people to unsubscribe. Include the actual cart contents with images and a link that restores the cart rather than dumping them on the homepage.

What I would not do: send a discount in the first email. You teach a segment of your customers to abandon deliberately, and the segment that learns this is the price-sensitive one you least want to train. If you discount at all, do it in the second message and make it look like a one-off.

The legal position matters here. Under UK GDPR and PECR you can email an existing customer about similar products under the soft opt-in, but a checkout abandoner who has never purchased is not an existing customer, and the safest reading is that you need consent. In practice most operators rely on legitimate interest for a short recovery sequence and document the balancing test. Get an actual view from someone qualified rather than from a plugin's marketing page.

Exit-intent popups on the checkout: no. On the cart, arguably. On the checkout itself you are interrupting someone who is trying to give you money, and the desktop-only mouse-out trigger means you are only interrupting a fraction of them anyway.

14. Accessibility In The Checkout, Which Is Where It Matters Most

Every accessibility failure in a checkout is a failure at the point of payment, which makes it more costly than the same failure anywhere else on the site.

The specific items, in the order I check them.

Every input has a real <label>. Not a placeholder. Placeholder-as-label disappears the moment someone types, fails contrast requirements in every implementation I have measured, and leaves the customer unable to check what a field was for when they review the form. Float the label above the field if the design demands it, but keep the element.

Errors are associated programmatically. aria-invalid on the field and aria-errormessage pointing at the message element, or aria-describedby if you need broader support. A red border and adjacent text is invisible to a screen reader user.

The submit failure moves focus. On a failed submit, focus goes to a summary listing the problems, each item linking to its field. This is the single highest-impact accessibility pattern in a checkout and it helps sighted keyboard users just as much.

Payment iframes are labelled. Stripe Elements, Adyen components and Braintree hosted fields all render into iframes, and an unlabelled iframe is announced as "frame" with no indication of what it wants. The title attribute on the iframe is the fix and it is one line.

Loading states are announced. A spinner during payment authorisation is silence to a screen reader user, during the twelve seconds when they most need to know something is happening.

<!-- Error summary. Rendered on submit failure, focused programmatically,
     with each item linking to the field it describes. -->
<div id="error-summary" role="alert" tabindex="-1" hidden>
  <h2>There are 2 problems with your details</h2>
  <ul>
    <li><a href="#postcode">Enter a postcode, for example SW1A 1AA</a></li>
    <li><a href="#card-number">Check your card number and try again</a></li>
  </ul>
</div>

<!-- Payment iframe: the title is what a screen reader announces. -->
<iframe src="https://js.stripe.com/..." title="Card number"></iframe>

<!-- Status region for authorisation. Present from first paint. -->
<p id="pay-status" role="status" aria-live="polite" class="sr-only"></p>

Testing this does not require a specialist. Unplug your mouse and complete a purchase using only the keyboard. If you cannot, neither can a substantial number of your customers, and you will find three or four problems in ten minutes. Then do it again with VoiceOver or NVDA, badly, as a novice — even incompetent screen reader testing catches the unlabelled iframe.

15. Why Most A/B Tests On This Cannot Work

Here is the part that annoys people, and I say it in every kickoff because it saves months.

Statistical power is not optional and most ecommerce checkout tests do not have it. The required sample size scales with the inverse square of the effect you want to detect, which means halving the detectable effect quadruples the traffic you need.

For a two-sided test at 95% confidence and 80% power, a workable approximation for the sample needed per variant is 16 × p × (1 − p) / δ², where p is your baseline conversion rate and δ is the absolute difference you want to detect.

BaselineRelative lift to detectAbsolute δVisitors per variantAt 40k/month, duration
2.0%+50%1.00pp~3,1005 days
2.0%+20%0.40pp~19,6001 month
2.0%+10%0.20pp~78,400~4 months
2.0%+5%0.10pp~313,600~16 months
2.0%+2%0.04pp~1,960,000~8 years

Read the bottom two rows and then think about the last time someone told you a button colour change produced a 3% lift after a two-week test on 12,000 visitors. That test could not detect a 3% relative change. What it detected was noise, and the reason the number looked convincing is that noise on small samples produces large apparent effects.

This is not a reason to stop testing. It is a reason to test only things large enough to be detectable at your traffic, and to accept judgement and evidence from elsewhere for everything smaller.

#!/usr/bin/env python3
"""Sample size for a two-proportion test. Run it BEFORE the test,
not after, and if the answer exceeds your traffic, do not run it."""
from math import ceil
from statistics import NormalDist

def sample_per_arm(baseline, rel_lift, alpha=0.05, power=0.80):
    p1 = baseline
    p2 = baseline * (1 + rel_lift)
    pbar = (p1 + p2) / 2
    z_a = NormalDist().inv_cdf(1 - alpha / 2)   # two-sided
    z_b = NormalDist().inv_cdf(power)
    num = (z_a * (2 * pbar * (1 - pbar)) ** 0.5
           + z_b * (p1 * (1 - p1) + p2 * (1 - p2)) ** 0.5) ** 2
    return ceil(num / (p2 - p1) ** 2)

for lift in (0.50, 0.20, 0.10, 0.05, 0.02):
    n = sample_per_arm(0.02, lift)
    # Two arms, and a business cycle: never stop mid-week even if
    # the calculator says you can.
    print(f"{lift:>5.0%} lift  ->  {n:>9,} per arm  ({2*n:>10,} total)")

Four practices that separate real testing from theatre.

Fix the sample size and duration before you start, and do not look until you reach it. Peeking at a running test and stopping when it crosses significance inflates the false positive rate dramatically — with daily checks over a fortnight the effective error rate lands somewhere near 25 to 30% rather than 5%.

Run for whole weeks. Weekend traffic converts differently from Tuesday traffic on every store I have looked at. A ten-day test is a nine-day test plus a bias.

Pick one primary metric before you start. If you measure conversion, AOV, revenue per session and step completion, one of them will look significant by chance. Choose the one you will act on and treat the rest as context.

Watch for a novelty period. Returning customers react to change as change. On a test running four weeks I discard the first three days for returning visitors, or segment new versus returning throughout.

What I do instead of testing small things: ship them, based on evidence from research that has the sample sizes I never will — Baymard's checkout usability work is built on thousands of hours of moderated testing across hundreds of sites — and spend my testing budget on the handful of changes big enough to measure. Radical redesigns, wallet placement, removing a whole step, changing the delivery proposition. Those produce effects in the range a real test can see.

16. A Worked Example, Including The Bit That Regressed

The footwear retailer again. Magento 2.4.5, about 40,000 checkout starts a month, 71% mobile, AOV around £74.

Week one: the regex. Removed the postcode pattern entirely, replaced with a server-side lookup that warns and permits. This was a two-hour fix and it recovered most of the 8% drift on its own, which made everything afterwards harder to attribute cleanly.

Weeks two and three: field reduction. 23 inputs to 11. Postcode lookup with a manual escape. Autocomplete tokens across the board. Guest checkout made the default and the forced-login-on-existing-email behaviour removed.

Week four: validation and errors. Reward-early-punish-late timing. Rewritten error copy with examples. Decline handling with per-code messages. Error summary with focus management.

Week five: wallets. Apple Pay and Google Pay on cart and at the top of checkout. This was the single biggest change and it took the longest, mostly because of the address-confirmation work.

Week six: trust and delivery. Named delivery dates replacing "3-5 working days". Returns policy line in the summary. Removed two security badge images and a testimonial carousel.

Results over twelve weeks against the prior year. Checkout completion rate from 51% to 67%. Address step drop-off from 19% to 8%. Mobile conversion up 34%, desktop up 11%. Field-level error events down 78%. Revenue per checkout-start up about 29%.

I would not claim the individual attributions with confidence. Six changes shipped in six weeks, in a seasonal window, and disentangling them properly would have required running each as a powered test, which at 40,000 starts a month would have taken most of a year. That trade — ship a bundle of well-evidenced changes and measure the bundle — is the right one for a store this size, and pretending otherwise is how consultants produce fictional attribution tables.

What regressed. The delivery date change caused a problem I did not anticipate. The dates were computed from the carrier's stated transit times and did not account for the retailer's own warehouse cut-off moving on Fridays, so orders placed on Friday afternoon showed a Monday date they could not meet. Late-delivery complaints rose about 40% over three weeks before the operations lead connected it to the change. A commitment you display is a commitment you have to keep, and I had treated the date as a UI problem when it was a fulfilment problem.

The other thing I got wrong: I removed the "confirm email" field, and the rate of orders with a mistyped email went up slightly. Not enough to reverse the decision, but enough that I now pair the removal with a typo-detection library that suggests corrections for common domain misspellings. Should have done that from the start.

17. Speed Is A Conversion Feature

Worth a section because it is the factor most often left out of a UX conversation and it interacts with everything above.

A checkout step that takes three seconds to respond to a button press produces double-submissions, back-button presses and abandonment, and no amount of good copy compensates. The specific latencies that matter are not page load — by checkout the customer has already loaded your site — but the response times of the address lookup, the delivery quote, and the payment authorisation.

Address lookup should return in under 400ms or the customer starts typing manually. Delivery quotes are frequently the worst offender because they call a third party synchronously; cache aggressively by postcode district and basket weight band, and if the quote cannot be produced quickly, show a sensible default and refine.

Payment authorisation genuinely takes seconds and cannot be made faster, so the design job is to make the wait tolerable: disable the button immediately, change its label to something specific like "Authorising with your bank", and announce the state to assistive technology. Never leave the button live, because customers do press it twice and your idempotency handling is probably not as good as you think.

The wider performance picture — what to measure and how to keep a checkout fast under load — is a separate discipline, and the layout-stability half of it in particular is covered in the Core Web Vitals piece. A form field that jumps as a validation message appears above it is a conversion problem before it is a metrics problem.

18. Questions I Get Asked

"Should we force account creation to build the list?" No. Offer it post-purchase where the only missing field is a password. You will get fewer accounts and they will be worth more, and your checkout will convert several points higher. This is the clearest cost-benefit in the whole article and it is still argued about weekly.

"One-page or multi-step?" An accordion on one page, which is both. But the step count is not the important variable. Visible running total, editable earlier sections, and preserved state on back matter more than the structure you pick.

"Is Shopify's checkout better than what we would build?" Almost certainly, yes, and I say that as someone who builds custom checkouts. It has been tested against traffic volumes no individual merchant will ever see. The argument for building your own is when you have genuinely unusual requirements — complex B2B pricing, split fulfilment, regulated products — and not because you want a different button colour.

"How many payment methods should we offer?" Cards, one wallet on each platform, PayPal, and one buy-now-pay-later option if your AOV justifies it. Beyond about six, the choice itself becomes friction and the tail methods take single-digit percentages while adding reconciliation work. I would rather have five well-implemented methods than eleven with two broken.

"Do progress indicators help?" Modestly, and mainly by setting expectations about length. They hurt if they lie — a four-step indicator on a flow that adds a fifth step conditionally is worse than none. Label the steps rather than numbering them.

"What about the discount code field?" Collapse it behind a text link, auto-apply codes arriving in the URL, and never give it visual weight comparable to the pay button. If a large fraction of your orders use codes, that is a pricing decision to revisit rather than a UI one.

"How do we know if our checkout is any good?" Two numbers. Completion rate from checkout start, and step-to-step drop. If completion is under about 50% you have a specific broken thing, not a general design problem, and it is usually findable in an afternoon by watching session recordings of abandonments at the worst step.

"Is it worth paying for a CRO agency?" It depends on your traffic, and specifically on whether you have enough of it to test at all. Under about 50,000 checkout starts a month you cannot run a meaningful testing programme, and what you actually need is someone to apply known-good patterns and fix what is broken. That is a week of work, not a retainer. Above a few hundred thousand, a real testing capability earns its cost.

"Should the checkout be on a subdomain?" Only if you have an architectural reason. It complicates analytics, session handling and cookies, and customers do not care. If you already have that split for platform reasons, make sure the visual continuity is exact — a checkout that looks like a different company is a genuine trust problem, and the security considerations around that boundary are worth reading up on in the secure checkout piece.

19. What I'd Do First

Ordered so that each step is cheap and informs the next.

One. Complete a purchase on your own site, on a phone, on mobile data, as a new customer, paying with a real card. Not a test card and not on your desk with a laptop. Most teams have not done this in a year and it surfaces two or three problems immediately.

Two. Instrument field-level validation errors. Every time a field shows an error, record which field and what the value was, hashed if it is personal. The footwear retailer's £180,000 regex would have shown up in this report on day one. Nothing else you can build gives that much diagnostic value for an hour of work.

Three. Count your fields and delete the ones nothing downstream consumes. Ask what system reads each field. If the answer is "nobody looks at it", the field is a tax you are levying for no reason.

Four. Fix the postcode or ZIP lookup and give it a visible manual escape. On a UK store this is the largest single improvement to the address step.

Five. Make guest checkout the default and move the account offer to the confirmation page.

Six. Change validation to blur-first, then input-once-errored, and rewrite every error message to include an example of a valid value.

Seven. Put wallets at the top of the checkout and on the cart, and confirm the returned address explicitly rather than trusting it.

Eight. Remove the security badge images and the visitor counter, and add a named delivery date and a returns line instead. Then check with operations that you can actually meet the date, which is the step I skipped.

Nine. Before you plan any test, run the sample size calculation for the effect you expect. If the answer is longer than six weeks of your traffic, do not run the test — ship the change on the strength of the evidence and measure the direction over a quarter.

The thing I would push back on hardest is the instinct to start with a redesign. Almost every checkout I have been handed had a specific broken thing in it — a regex, a required field nobody needed, a login wall, a validation message that fired on the wrong event — and the broken thing was worth more than the redesign. Find it first. It is usually one afternoon and a report you have not built yet.

Suggested & Related Reading

Explore related engineering guides from Kenneth D'Silva: