E-Commerce Checkout Accessibility: Complete WCAG Compliance Guide
E-commerce checkout accessibility determines whether customers with disabilities can complete purchases on your site. An inaccessible checkout means lost sales from approximately 27% of American adults with disabilities—and significant legal exposure. E-commerce represents the largest category of ADA web accessibility lawsuits, with checkout flow failures specifically cited in litigation.
This guide covers making your entire checkout process WCAG 2.1 AA compliant: cart functionality, shipping forms, payment processing, and order confirmation. Every step must work for keyboard-only users, screen reader users, and users with various disabilities.
Q: What makes a checkout WCAG compliant?
A: WCAG-compliant checkout requires: all form fields have programmatic labels, error messages identify problems and suggest fixes, the entire flow is keyboard navigable, focus management works correctly, color isn't the only indicator of status, and progress is clearly communicated throughout.
Cart Accessibility
Product Display in Cart
Each cart item needs clear, accessible identification:
<article class="cart-item" aria-labelledby="item-1-name">
<img src="product.jpg"
alt="Blue Nike Air Max 90 running shoes, size 10">
<div class="item-details">
<h2 id="item-1-name">Nike Air Max 90</h2>
<p>Color: Blue | Size: 10</p>
<p class="price" aria-label="Price: $129.00">$129.00</p>
</div>
<div class="quantity-control">
<label for="qty-1">Quantity</label>
<input type="number"
id="qty-1"
name="quantity"
value="1"
min="1"
max="10"
aria-describedby="qty-1-update">
<span id="qty-1-update" class="visually-hidden">
Changes update cart automatically
</span>
</div>
<button type="button"
aria-label="Remove Nike Air Max 90 from cart">
Remove
</button>
</article>Quantity Updates
When quantities change, communicate updates to screen readers:
<div aria-live="polite" id="cart-status" class="visually-hidden">
<!-- Updated dynamically -->
</div>
<script>
function updateQuantity(itemId, newQty) {
// Update cart logic
updateCart(itemId, newQty);
// Announce to screen readers
const status = document.getElementById('cart-status');
status.textContent = `Quantity updated. Cart total: ${newTotal}`;
}
</script>Remove Item Functionality
Remove buttons need accessible names identifying what's being removed:
<!-- Wrong: Generic button -->
<button>Ă—</button>
<!-- Right: Descriptive accessible name -->
<button aria-label="Remove Blue Running Shoes from cart">
<span aria-hidden="true">Ă—</span>
<span class="visually-hidden">Remove</span>
</button>After removal, manage focus appropriately:
function removeItem(itemElement) {
const nextItem = itemElement.nextElementSibling;
const prevItem = itemElement.previousElementSibling;
// Remove the item
itemElement.remove();
// Move focus to next item, previous, or cart heading
if (nextItem) {
nextItem.querySelector('h2, a').focus();
} else if (prevItem) {
prevItem.querySelector('h2, a').focus();
} else {
document.querySelector('.cart-empty-message').focus();
}
// Announce removal
announceToScreenReader('Item removed from cart');
}Cart Summary
Provide clear summary accessible to all users:
<section aria-labelledby="cart-summary-heading">
<h2 id="cart-summary-heading">Order Summary</h2>
<dl class="summary-list">
<dt>Subtotal</dt>
<dd>$258.00</dd>
<dt>Shipping</dt>
<dd>$9.99</dd>
<dt>Tax</dt>
<dd>$21.44</dd>
<dt><strong>Total</strong></dt>
<dd><strong>$289.43</strong></dd>
</dl>
<button type="submit" class="checkout-button">
Proceed to Checkout
<span class="visually-hidden">
- 2 items, $289.43 total
</span>
</button>
</section>Checkout Form Accessibility
Progress Indication
Multi-step checkout needs clear progress communication:
<nav aria-label="Checkout progress">
<ol class="progress-steps">
<li aria-current="step">
<span class="step-number">1</span>
<span class="step-name">Shipping</span>
</li>
<li>
<span class="step-number">2</span>
<span class="step-name">Payment</span>
</li>
<li>
<span class="step-number">3</span>
<span class="step-name">Review</span>
</li>
</ol>
</nav>Shipping Information Form
Every field needs proper labeling and autocomplete:
<form id="shipping-form">
<fieldset>
<legend>Contact Information</legend>
<div class="form-field">
<label for="email">
Email address
<span class="required" aria-hidden="true">*</span>
</label>
<input type="email"
id="email"
name="email"
autocomplete="email"
required
aria-required="true">
</div>
<div class="form-field">
<label for="phone">
Phone number
<span class="optional">(optional)</span>
</label>
<input type="tel"
id="phone"
name="phone"
autocomplete="tel">
</div>
</fieldset>
<fieldset>
<legend>Shipping Address</legend>
<div class="form-field">
<label for="full-name">
Full name
<span class="required" aria-hidden="true">*</span>
</label>
<input type="text"
id="full-name"
name="name"
autocomplete="name"
required
aria-required="true">
</div>
<div class="form-field">
<label for="address">
Street address
<span class="required" aria-hidden="true">*</span>
</label>
<input type="text"
id="address"
name="address"
autocomplete="street-address"
required
aria-required="true">
</div>
<div class="form-field">
<label for="address2">
Apartment, suite, etc.
<span class="optional">(optional)</span>
</label>
<input type="text"
id="address2"
name="address2"
autocomplete="address-line2">
</div>
<div class="form-row">
<div class="form-field">
<label for="city">
City
<span class="required" aria-hidden="true">*</span>
</label>
<input type="text"
id="city"
name="city"
autocomplete="address-level2"
required
aria-required="true">
</div>
<div class="form-field">
<label for="state">
State
<span class="required" aria-hidden="true">*</span>
</label>
<select id="state"
name="state"
autocomplete="address-level1"
required
aria-required="true">
<option value="">Select state</option>
<option value="AL">Alabama</option>
<!-- More states -->
</select>
</div>
<div class="form-field">
<label for="zip">
ZIP code
<span class="required" aria-hidden="true">*</span>
</label>
<input type="text"
id="zip"
name="zip"
autocomplete="postal-code"
required
aria-required="true"
pattern="[0-9]{5}(-[0-9]{4})?"
aria-describedby="zip-format">
<span id="zip-format" class="hint">
5-digit or 9-digit format
</span>
</div>
</div>
</fieldset>
<button type="submit">Continue to Payment</button>
</form>Shipping Options
Radio button groups need proper structure:
<fieldset>
<legend>Shipping Method</legend>
<div class="shipping-option">
<input type="radio"
id="ship-standard"
name="shipping"
value="standard"
checked>
<label for="ship-standard">
<span class="option-name">Standard Shipping</span>
<span class="option-time">5-7 business days</span>
<span class="option-price">Free</span>
</label>
</div>
<div class="shipping-option">
<input type="radio"
id="ship-express"
name="shipping"
value="express">
<label for="ship-express">
<span class="option-name">Express Shipping</span>
<span class="option-time">2-3 business days</span>
<span class="option-price">$12.99</span>
</label>
</div>
<div class="shipping-option">
<input type="radio"
id="ship-overnight"
name="shipping"
value="overnight">
<label for="ship-overnight">
<span class="option-name">Overnight Shipping</span>
<span class="option-time">Next business day</span>
<span class="option-price">$24.99</span>
</label>
</div>
</fieldset>Payment Form Accessibility
Credit Card Fields
Payment fields require special attention:
<fieldset>
<legend>Payment Information</legend>
<div class="form-field">
<label for="cc-name">
Name on card
<span class="required" aria-hidden="true">*</span>
</label>
<input type="text"
id="cc-name"
name="cc-name"
autocomplete="cc-name"
required
aria-required="true">
</div>
<div class="form-field">
<label for="cc-number">
Card number
<span class="required" aria-hidden="true">*</span>
</label>
<input type="text"
id="cc-number"
name="cc-number"
autocomplete="cc-number"
inputmode="numeric"
required
aria-required="true"
aria-describedby="cc-icons">
<div id="cc-icons" class="card-icons">
<span class="visually-hidden">We accept Visa, Mastercard, American Express, and Discover</span>
<img src="visa.svg" alt="" aria-hidden="true">
<img src="mc.svg" alt="" aria-hidden="true">
<img src="amex.svg" alt="" aria-hidden="true">
<img src="discover.svg" alt="" aria-hidden="true">
</div>
</div>
<div class="form-row">
<div class="form-field">
<label for="cc-exp">
Expiration date
<span class="required" aria-hidden="true">*</span>
</label>
<input type="text"
id="cc-exp"
name="cc-exp"
autocomplete="cc-exp"
placeholder="MM/YY"
required
aria-required="true"
aria-describedby="exp-format">
<span id="exp-format" class="hint">Format: MM/YY</span>
</div>
<div class="form-field">
<label for="cc-csc">
Security code
<span class="required" aria-hidden="true">*</span>
</label>
<input type="text"
id="cc-csc"
name="cc-csc"
autocomplete="cc-csc"
inputmode="numeric"
maxlength="4"
required
aria-required="true"
aria-describedby="csc-help">
<span id="csc-help" class="hint">
3 or 4 digits on back of card
</span>
</div>
</div>
</fieldset>Payment Method Selection
When offering multiple payment methods:
<fieldset>
<legend>Payment Method</legend>
<div class="payment-method">
<input type="radio"
id="pay-card"
name="payment-method"
value="card"
checked
aria-controls="card-fields">
<label for="pay-card">Credit or Debit Card</label>
</div>
<div class="payment-method">
<input type="radio"
id="pay-paypal"
name="payment-method"
value="paypal"
aria-controls="paypal-section">
<label for="pay-paypal">PayPal</label>
</div>
<div class="payment-method">
<input type="radio"
id="pay-applepay"
name="payment-method"
value="applepay"
aria-controls="applepay-section">
<label for="pay-applepay">Apple Pay</label>
</div>
</fieldset>
<!-- Card fields shown when card selected -->
<div id="card-fields" class="payment-details">
<!-- Credit card form fields -->
</div>
<!-- PayPal section shown when selected -->
<div id="paypal-section" class="payment-details" hidden>
<p>You'll be redirected to PayPal to complete your purchase.</p>
<button type="button">Continue with PayPal</button>
</div>Third-Party Payment Widgets
Third-party payment processors (Stripe, Braintree) should be tested for accessibility:
Common issues:
- iframe focus management
- Error message accessibility
- Keyboard navigation within hosted fields
- Screen reader announcements
Testing approach:
- Tab through payment section
- Complete payment with screen reader
- Trigger validation errors and verify announcement
- Test on mobile with VoiceOver/TalkBack
Error Handling
Validation Error Display
Errors must be clearly associated with fields:
<div class="form-field error">
<label for="email">Email address</label>
<input type="email"
id="email"
name="email"
aria-invalid="true"
aria-describedby="email-error">
<span id="email-error" class="error-message" role="alert">
Please enter a valid email address (example: name@domain.com)
</span>
</div>Error Summary
For multiple errors, provide summary with links:
<div role="alert" class="error-summary" tabindex="-1" id="error-summary">
<h2>Please fix the following errors:</h2>
<ul>
<li><a href="#email">Email address is required</a></li>
<li><a href="#cc-number">Card number is invalid</a></li>
<li><a href="#cc-exp">Expiration date has passed</a></li>
</ul>
</div>
<script>
function handleFormErrors(errors) {
// Populate error summary
populateErrorSummary(errors);
// Move focus to error summary
document.getElementById('error-summary').focus();
}
</script>Real-Time Validation
If validating as users type, don't be overly aggressive:
// Validate on blur, not every keystroke
input.addEventListener('blur', validateField);
// Don't show errors while user is still typing
input.addEventListener('focus', () => {
clearFieldError(input);
});Order Review and Confirmation
Review Page Accessibility
Before final submission, present clear review:
<section aria-labelledby="review-heading">
<h1 id="review-heading">Review Your Order</h1>
<section aria-labelledby="shipping-review">
<h2 id="shipping-review">Shipping Information</h2>
<address>
John Smith<br>
123 Main Street<br>
Anytown, CA 90210
</address>
<a href="#shipping-step">Edit shipping information</a>
</section>
<section aria-labelledby="payment-review">
<h2 id="payment-review">Payment Method</h2>
<p>Visa ending in 4242</p>
<a href="#payment-step">Edit payment method</a>
</section>
<section aria-labelledby="items-review">
<h2 id="items-review">Order Items</h2>
<!-- Cart items list -->
</section>
<section aria-labelledby="total-review">
<h2 id="total-review">Order Total</h2>
<!-- Order summary -->
</section>
<button type="submit" class="place-order">
Place Order - $289.43
</button>
</section>Order Confirmation
Confirmation page needs clear success indication:
<main>
<section aria-labelledby="confirmation-heading">
<h1 id="confirmation-heading">
<span class="visually-hidden">Success: </span>
Order Confirmed!
</h1>
<p class="order-number">
Order number: <strong>#12345678</strong>
</p>
<p>
We've sent a confirmation email to
<strong>customer@example.com</strong>
</p>
<!-- Order details -->
</section>
</main>
<script>
// Announce success to screen readers
document.addEventListener('DOMContentLoaded', () => {
const heading = document.getElementById('confirmation-heading');
heading.focus();
});
</script>Common Checkout Accessibility Issues
Issues TestParty Identifies
TestParty's Shopify and e-commerce scanning catches:
- Form labels: Missing or improper label associations
- Error handling: Inaccessible error messages
- Focus management: Focus not moving to errors or confirmations
- Keyboard traps: Payment widgets trapping keyboard users
- Color contrast: Low contrast in form fields and errors
- ARIA issues: Incorrect states and properties
Fixes TestParty Provides
For e-commerce sites, TestParty generates specific code fixes:
- Proper label associations for checkout fields
- Accessible error message patterns
- Focus management scripts
- ARIA attributes for custom widgets
- Contrast-compliant color alternatives
Testing Checkout Accessibility
Automated Testing
TestParty scans checkout flows for programmatically-detectable issues. Run scans after any checkout changes—theme updates, app installations, or customizations.
Manual Testing Checklist
Keyboard navigation:
- [ ] Can complete entire checkout with keyboard only
- [ ] Tab order follows logical flow
- [ ] No keyboard traps in payment fields
- [ ] Focus visible throughout
- [ ] Can select all shipping/payment options
Screen reader testing:
- [ ] All fields have announced labels
- [ ] Required status announced
- [ ] Error messages announced
- [ ] Progress indicator accessible
- [ ] Order summary readable
Visual accessibility:
- [ ] Form labels visible (not placeholder-only)
- [ ] Errors indicated beyond color
- [ ] Sufficient color contrast
- [ ] Content readable at 200% zoom
FAQ Section
Q: Should I use a single-page or multi-step checkout?
A: Both can be accessible. Multi-step checkout with clear progress indication often works better for screen reader users—less overwhelming than one long form. Single-page checkout with clear sections can also work. Key is clear structure and feedback regardless of format.
Q: How do I make third-party payment forms accessible?
A: Test payment provider's hosted fields for keyboard navigation and screen reader compatibility. Report issues to the provider. Consider alternative providers if accessibility issues persist. Document any limitations in your accessibility statement.
Q: Is guest checkout more accessible than account checkout?
A: Guest checkout reduces cognitive load and form completion burden—beneficial for users with cognitive disabilities. Offer both options clearly. Don't force account creation to complete purchase.
Q: How should I handle address autocomplete accessibility?
A: Address autocomplete dropdowns need keyboard navigation, screen reader announcements for suggestions, and ability to ignore autocomplete. Test with assistive technology before deployment.
Q: What about mobile checkout accessibility?
A: Test with iOS VoiceOver and Android TalkBack. Ensure touch targets are at least 44Ă—44 pixels. Forms should work with mobile screen readers. Payment methods like Apple Pay/Google Pay should be keyboard accessible.
Key Takeaways
- Every checkout field needs a programmatic label. No exceptions—screen reader users must know what to enter.
- Error messages must identify problem and solution. "Invalid" isn't enough; explain what's wrong and how to fix it.
- Focus management is critical. Move focus to errors on validation failure, to confirmation on success.
- Test the entire flow, not just individual fields. Complete a purchase using only keyboard, then with screen reader.
- Third-party payment widgets need testing. Don't assume Stripe/PayPal/etc. are accessible—verify.
- Continuous monitoring catches regressions. Checkout changes frequently; test after every update.
Conclusion
Checkout accessibility directly impacts revenue and legal risk. An inaccessible checkout turns away customers with disabilities and creates lawsuit exposure. The fundamentals—proper labels, clear errors, keyboard navigation, screen reader compatibility—aren't difficult to implement but require attention throughout the checkout flow.
TestParty identifies checkout accessibility issues and provides specific fixes for e-commerce platforms including Shopify. Continuous monitoring catches problems as your checkout evolves with theme updates, app changes, and customizations.
Ready to fix your checkout accessibility? Get a free accessibility scan to identify issues in your checkout flow.
Related Articles:
- Shopify Accessibility Guide: Complete WCAG Compliance
- Form Accessibility Guide: Labels, Errors, and WCAG Compliance
TestParty's content team produced this article using AI-powered research tools combined with our expertise in automated accessibility testing. The guidance here reflects current best practices but shouldn't substitute for professional legal counsel on ADA or WCAG compliance matters.
Stay informed
Accessibility insights delivered
straight to your inbox.


Automate the software work for accessibility compliance, end-to-end.
Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.
Book a Demo