E-commerce checkout accessibility: what breaks and how to fix it

Checkout is where most e-commerce accessibility failures matter most. A blind user who can navigate your product pages, add items to a cart, and reach the checkout has accomplished the browsing part of a purchase. If they cannot complete the transaction because form fields are unlabeled, error messages are silent, or the payment step is unreachable by keyboard, the barrier falls at the moment of economic exchange.

Checkout flows also change frequently. New payment providers get integrated, A/B tests rearrange form fields, promotional code inputs get added, loyalty point redemption widgets appear. Each change can silently break something that was previously working. This makes checkout the highest-value area to monitor for accessibility regressions.

This guide covers the failures we see most often in checkout flows and how to fix them.

1. Form fields without labels

The most common checkout accessibility failure is using a placeholder attribute as the only label for an input field. Placeholder text works visually until the user starts typing, at which point it disappears. Screen reader behavior varies, but a field with no label and a visible placeholder will be announced as "edit text" with no context once the field has a value.

This pattern is extremely common in checkout forms:

<!-- Common but wrong: placeholder used as the only label -->
<input type="text" placeholder="First name" name="first_name">
<input type="email" placeholder="Email address" name="email">

The fix is a persistent visible label associated with each field via matching for and id attributes:

<!-- Correct: visible label associated with field -->
<label for="first-name">First name</label>
<input type="text" id="first-name" name="first_name"
  autocomplete="given-name">

<label for="email">Email address</label>
<input type="email" id="email" name="email"
  autocomplete="email" placeholder="you@example.com">

The placeholder can still appear as a hint when the field is empty. The label is what identifies the field permanently. Both should be present in most cases.

Required field indicators also need labels, not just visual asterisks. If you mark required fields with *, include a legend explaining the convention, or use aria-required="true" on the input so screen readers announce the requirement without relying on the visual marker.

2. Error messages that are not announced

When a user submits a checkout form and validation fails, the errors need to reach screen reader users in two ways: the errors need to be announced automatically when they appear, and each error needs to be programmatically linked to its field so a user navigating the form knows which field has a problem.

A typical implementation that fails both requirements:

<!-- No connection between field and error; error is not announced -->
<label for="email">Email address</label>
<input type="email" id="email" class="field-error">
<span class="error">Please enter a valid email address</span>

The red border on the field is a visual indicator only. The error message is visible but has no programmatic relationship to the field and will not be announced when it is inserted into the DOM.

A fixed version:

<label for="email">Email address</label>
<input type="email" id="email"
  aria-describedby="email-error"
  aria-invalid="true">
<span id="email-error" role="alert">
  Please enter a valid email address
</span>

Three changes work together here. The aria-describedby attribute links the field to its error message by ID, so when a screen reader user lands on the field, the error is read as part of the field description. The aria-invalid="true" attribute signals that the current value is invalid, which most screen readers announce. The role="alert" on the error element causes it to be announced immediately when it is inserted into the DOM, without the user needing to navigate to it.

If you use a single error summary at the top of the form (listing all errors at once), move focus to that summary after form submission so screen reader users are directed to it. A summary without a focus shift requires the user to navigate back to the top of the page, which they may not know to do.

3. The autocomplete attribute on checkout fields

The autocomplete attribute tells browsers what type of information a field expects. For screen reader users, many assistive technologies also use this attribute to provide additional context when reading the field. For all users, it enables reliable autofill from saved addresses and payment details.

Checkout fields with well-matched autocomplete values:

<!-- Contact information -->
<input type="text" id="full-name" autocomplete="name">
<input type="email" id="email" autocomplete="email">
<input type="tel" id="phone" autocomplete="tel">

<!-- Shipping address -->
<input type="text" id="address1" autocomplete="shipping address-line1">
<input type="text" id="address2" autocomplete="shipping address-line2">
<input type="text" id="city" autocomplete="shipping address-level2">
<input type="text" id="state" autocomplete="shipping address-level1">
<input type="text" id="postal" autocomplete="shipping postal-code">
<input type="text" id="country" autocomplete="shipping country">

<!-- Card payment (for self-hosted payment forms) -->
<input type="text" id="cc-name" autocomplete="cc-name">
<input type="text" id="cc-number" autocomplete="cc-number">
<input type="text" id="cc-exp" autocomplete="cc-exp">
<input type="text" id="cc-cvc" autocomplete="cc-csc">

The section tokens (shipping, billing) allow browsers to distinguish between duplicate field types when both shipping and billing address sections are present in the same form.

4. Payment fields and third-party iframes

Payment card inputs embedded by providers like Stripe, PayPal, Square, or Braintree render inside cross-origin iframes that you do not control. The accessibility of those specific input fields is the payment provider's responsibility, not yours. You cannot add aria-describedby or label attributes to elements inside a cross-origin iframe.

What you do control around a payment section:

  • The visible heading that introduces the payment area. Use a proper heading element (h2 or h3 depending on page structure) rather than styled text.
  • Error messages your checkout code displays when a payment fails. These should follow the same role="alert" and focus management rules as other form errors.
  • The submit button label. "Place order" or "Pay $X.XX" is more descriptive than "Submit." When the form is processing, the button's accessible state should communicate that: aria-disabled="true" plus a loading indicator with aria-live="polite" for a status update.

For payment providers that are open to customization, some offer accessibility options. Stripe Elements, for example, provides properties for setting placeholder text and font size that affect the visual rendering inside the iframe, even though the iframe markup itself is not editable.

5. Multi-step checkout progress

Many checkouts use a step indicator showing Cart, Shipping, Payment, and Confirmation. If this is implemented with styled divs and CSS alone, it conveys no information to screen reader users about which step they are currently on.

A navigable progress indicator:

<nav aria-label="Checkout progress">
  <ol>
    <li><a href="/checkout/cart">Cart</a></li>
    <li aria-current="step">Shipping</li>
    <li><span aria-hidden="true">Payment</span></li>
    <li><span aria-hidden="true">Confirmation</span></li>
  </ol>
</nav>

The aria-current="step" attribute marks the active step. Most screen readers announce it as "current" when the element is read. Steps that have been completed can be links back to that step; steps not yet reached should not be interactive.

When a user clicks "Continue to payment" and the page transitions to the payment step, focus needs to move. If the checkout is a single-page experience with JavaScript-driven step changes, focus sitting on the button that was just clicked means a screen reader user is at the end of the shipping form with no indication they have moved to a new step. Move focus to the payment section heading:

// After rendering the payment step
const heading = document.querySelector('#payment-step-heading');
if (heading) {
  heading.setAttribute('tabindex', '-1');
  heading.focus();
}

Setting tabindex="-1" allows programmatic focus on an element that is not normally focusable (headings are not in the tab order). Remove the attribute after focus has shifted if you do not want it to persist in the tab order.

6. Order summary tables

Order summary sections often list items, quantities, and prices. When this is rendered as a visual layout with flexbox or grid and no semantic structure, screen reader users hear a flat list of text with no column or row context.

<!-- Accessible order summary table -->
<table>
  <caption>Order summary</caption>
  <thead>
    <tr>
      <th scope="col">Item</th>
      <th scope="col">Qty</th>
      <th scope="col">Price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Blue linen shirt (M)</td>
      <td>1</td>
      <td>$68.00</td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <th scope="row" colspan="2">Subtotal</th>
      <td>$68.00</td>
    </tr>
    <tr>
      <th scope="row" colspan="2">Shipping</th>
      <td>$0.00</td>
    </tr>
    <tr>
      <th scope="row" colspan="2">Total</th>
      <td>$68.00</td>
    </tr>
  </tfoot>
</table>

The scope attribute on header cells tells screen readers how to interpret each header: scope="col" for column headers and scope="row" for row headers within the table body and foot. The <caption> element gives the table an accessible name that is read when a screen reader user first encounters it.

7. Order confirmation page

The order confirmation page completes the transaction. Two things to check here: whether focus is managed correctly when the page loads, and whether the confirmation information is structured so it can be read in sequence.

If the confirmation is a full page load (not a single-page transition), the browser will place focus at the top of the page on load, which is correct behavior. If it is a SPA transition that renders the confirmation into the same page layout, focus needs to move to the confirmation heading, using the same programmatic focus technique described for checkout steps.

The order number should be in the page heading or immediately below it, not buried inside a summary table. A screen reader user who reaches the confirmation after a successful checkout should hear the confirmation message and order reference without needing to navigate a layout to find them.

Why checkout benefits most from regression monitoring

Browsing pages on an e-commerce site change slowly: product images update, collections rotate, copy gets revised. Checkout flows change for different reasons: payment provider integrations, promotional mechanics, split-testing, shipping option changes. A component that was added to the checkout six months ago and scanned as clean may be broken by a framework update or a new payment method integration.

Automated scanning after each deploy catches the class of failures described in this guide: a label that lost its for attribute during a template refactor, an error message that no longer has an ID to match its aria-describedby, a new form field added without a label. These are structural changes visible to a scanner even without visual rendering, which is what makes automated monitoring practical for checkout specifically.

BarrierScan monitors e-commerce sites automatically and sends a diff report when findings change. If your checkout has accessibility issues, a free scan will surface them with the specific element, WCAG criterion, and a code example of what to change. Request a scan on the home page.