How to Fix Your E-commerce Checkout for Screen Readers
TABLE OF CONTENTS
Screen reader users cannot complete purchases when checkout forms lack proper labels, errors aren't announced, or focus management fails. This guide provides specific, implementable fixes for the accessibility issues that block blind and low-vision customers from buying. Checkout accessibility failures appear in the majority of e-commerce lawsuits—fixing these issues both prevents legal exposure and captures sales from the 28.7% of US adults living with disabilities.
Prerequisites
Before implementing these fixes, you'll need some basic knowledge and tools.
Required knowledge: HTML form elements. Basic CSS. Your e-commerce platform's template system (Liquid for Shopify, Stencil for BigCommerce, PHP for WooCommerce).
Tools for testing: Screen reader (VoiceOver on Mac, NVDA on Windows—both free). Browser developer tools. WAVE browser extension for quick validation.
Time estimate: 4-8 hours for basic fixes on standard platform checkouts. More for heavily customized implementations.
Step 1: Add Proper Form Labels
Missing form labels are the most common checkout accessibility failure. Screen reader users hear "edit, blank" without knowing what information to enter.
The Problem
Many checkout implementations use placeholder text instead of labels:
<!-- This doesn't work for screen readers -->
<input type="text" placeholder="Email address">
<input type="text" placeholder="First name">
<input type="text" placeholder="Address">Placeholder text disappears when users start typing. Screen readers don't reliably announce placeholders as labels. This fails WCAG criterion 3.3.2 (Labels or Instructions).
The Fix
Every form field needs a visible, programmatically associated label:
<!-- Proper implementation -->
<div class="form-group">
<label for="checkout-email">Email address</label>
<input type="email" id="checkout-email" autocomplete="email" required>
</div>
<div class="form-group">
<label for="checkout-firstname">First name</label>
<input type="text" id="checkout-firstname" autocomplete="given-name" required>
</div>
<div class="form-group">
<label for="checkout-address">Street address</label>
<input type="text" id="checkout-address" autocomplete="street-address" required>
</div>Key Requirements
The `for` attribute must match the input's `id`. Labels must be visible—don't use CSS to hide them. Include autocomplete attributes for standard fields (see Step 3). Use descriptive text that explains what's needed.
Testing Your Fix
Open your checkout with VoiceOver (Cmd+F5 on Mac) or NVDA (free download for Windows). Tab to each form field. You should hear the label read aloud: "Email address, edit text." If you hear "edit, blank" or just the placeholder, the fix isn't working.
Step 2: Implement Accessible Error Handling
When checkout validation fails, screen reader users need clear indication of what went wrong. Most checkouts handle this poorly.
The Problem
Common error handling approaches fail accessibility:
<!-- Error shown only with color change - invisible to screen readers -->
<input type="email" class="error-border">
<!-- Error message far from field - hard to associate -->
<div class="error-banner">Please fix the errors below</div>
<!-- Generic message - doesn't help user fix problem -->
<span class="error">Invalid input</span>The Fix
Implement error handling that announces to screen readers and associates errors with fields:
<div class="form-group">
<label for="checkout-email">Email address</label>
<input
type="email"
id="checkout-email"
aria-describedby="email-error"
aria-invalid="true"
autocomplete="email">
<span id="email-error" class="error-message" role="alert">
Please enter a valid email address (example: name@domain.com)
</span>
</div>Key Requirements
Use `aria-describedby` to associate error messages with fields. Add `aria-invalid="true"` to fields with errors. Use `role="alert"` to announce errors immediately. Write specific error messages that explain how to fix the problem.
Focus Management on Submit
When form submission fails, move focus to the first error:
// After validation fails
const firstError = document.querySelector('[aria-invalid="true"]');
if (firstError) {
firstError.focus();
// Optionally announce summary
announceToScreenReader('Form has 3 errors. First error: Email address');
}
// Helper function for announcements
function announceToScreenReader(message) {
const announcement = document.createElement('div');
announcement.setAttribute('role', 'alert');
announcement.setAttribute('aria-live', 'assertive');
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
setTimeout(() => announcement.remove(), 1000);
}CSS for Screen Reader-Only Content
The helper class for announcements:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}Testing Your Fix
Intentionally submit the form with invalid data. Screen readers should announce the error message immediately. Focus should move to the first problem field. The association between field and error should be clear.
Step 3: Add Autocomplete Attributes
Autocomplete attributes help users complete forms faster and with fewer errors. They're required for WCAG 2.1 Level AA compliance (criterion 1.3.5).
Standard Checkout Autocomplete Values
Add these attributes to your checkout fields:
<!-- Personal Information -->
<input type="text" autocomplete="name">
<input type="text" autocomplete="given-name">
<input type="text" autocomplete="family-name">
<input type="email" autocomplete="email">
<input type="tel" autocomplete="tel">
<!-- Address -->
<input type="text" autocomplete="street-address">
<input type="text" autocomplete="address-line1">
<input type="text" autocomplete="address-line2">
<input type="text" autocomplete="address-level2"> <!-- City -->
<input type="text" autocomplete="address-level1"> <!-- State/Province -->
<input type="text" autocomplete="postal-code">
<input type="text" autocomplete="country-name">
<!-- Payment -->
<input type="text" autocomplete="cc-name">
<input type="text" autocomplete="cc-number">
<input type="text" autocomplete="cc-exp">
<input type="text" autocomplete="cc-csc">Separate Shipping and Billing
When checkout has both shipping and billing addresses, differentiate them:
<!-- Shipping Address -->
<input type="text" autocomplete="shipping street-address">
<input type="text" autocomplete="shipping address-level2">
<!-- Billing Address -->
<input type="text" autocomplete="billing street-address">
<input type="text" autocomplete="billing address-level2">Step 4: Fix Keyboard Navigation
Screen reader users navigate primarily via keyboard. Checkout must work without a mouse.
Ensure All Elements Are Reachable
Tab through your entire checkout. Every interactive element must be reachable:
<!-- Interactive elements should be focusable -->
<button type="button">Apply coupon</button> <!-- Naturally focusable -->
<a href="/cart">Edit cart</a> <!-- Naturally focusable -->
<!-- Custom elements need tabindex -->
<div role="button" tabindex="0" onclick="selectPayment('paypal')">
PayPal
</div>Provide Visible Focus Indicators
Users need to see which element is focused:
/* Don't do this */
*:focus {
outline: none;
}
/* Do this instead */
input:focus,
select:focus,
button:focus,
a:focus,
[tabindex]:focus {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
/* High contrast for dark backgrounds */
.dark-section input:focus,
.dark-section button:focus {
outline-color: #ffffff;
}Handle Modals and Drawers
When checkout opens modals (address selection, payment confirmation), manage focus:
// Opening modal
function openModal(modalElement) {
modalElement.style.display = 'block';
modalElement.setAttribute('aria-hidden', 'false');
// Move focus to first focusable element
const firstFocusable = modalElement.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable.focus();
// Trap focus inside modal
modalElement.addEventListener('keydown', trapFocus);
}
// Closing modal
function closeModal(modalElement, returnFocus) {
modalElement.style.display = 'none';
modalElement.setAttribute('aria-hidden', 'true');
modalElement.removeEventListener('keydown', trapFocus);
// Return focus to trigger element
returnFocus.focus();
}
// Focus trap function
function trapFocus(e) {
if (e.key !== 'Tab') return;
const focusableElements = this.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusableElements[0];
const last = focusableElements[focusableElements.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}Testing Keyboard Navigation
- Start at the beginning of checkout
- Press Tab repeatedly through the entire flow
- Can you reach every button, link, and form field?
- Are focus indicators visible on each element?
- Can you complete a purchase using only Tab, Enter, Space, and Escape?
Step 5: Announce Dynamic Content
Checkout often updates content without page reloads—cart totals, shipping options, validation messages. Screen readers need to know about these changes.
Use ARIA Live Regions
For content that updates dynamically:
<!-- Cart total that updates -->
<div aria-live="polite" aria-atomic="true">
Order total: <span id="order-total">$125.00</span>
</div>
<!-- Shipping option selection -->
<div role="status" aria-live="polite">
<span id="shipping-announcement"></span>
</div>When updating:
// Update cart total
function updateTotal(newTotal) {
document.getElementById('order-total').textContent = newTotal;
// aria-live region automatically announces the change
}
// Announce shipping selection
function selectShipping(option) {
document.getElementById('shipping-announcement').textContent =
`${option.name} selected. Delivery in ${option.days} days. Cost: ${option.price}`;
}Loading States
When checkout processes (applying coupons, calculating shipping):
<!-- Loading indicator -->
<div role="status" aria-live="polite" id="loading-status" class="sr-only">
Calculating shipping options...
</div>Step 6: Make Payment Selection Accessible
Payment method selection often fails accessibility. Radio buttons, accordion panels, or custom UI all need proper implementation.
Accessible Radio Button Group
<fieldset>
<legend>Payment method</legend>
<div class="payment-option">
<input type="radio" id="pay-card" name="payment" value="card" checked>
<label for="pay-card">
Credit or debit card
</label>
</div>
<div class="payment-option">
<input type="radio" id="pay-paypal" name="payment" value="paypal">
<label for="pay-paypal">
PayPal
</label>
</div>
<div class="payment-option">
<input type="radio" id="pay-applepay" name="payment" value="applepay">
<label for="pay-applepay">
Apple Pay
</label>
</div>
</fieldset>Custom Payment Buttons
If using custom button UI instead of native radio buttons:
<div role="radiogroup" aria-label="Payment method">
<button
role="radio"
aria-checked="true"
class="payment-btn selected"
onclick="selectPayment('card')">
Credit or debit card
</button>
<button
role="radio"
aria-checked="false"
class="payment-btn"
onclick="selectPayment('paypal')">
PayPal
</button>
</div>Update `aria-checked` when selection changes:
function selectPayment(method) {
// Update all buttons
document.querySelectorAll('[role="radio"]').forEach(btn => {
btn.setAttribute('aria-checked', 'false');
btn.classList.remove('selected');
});
// Set selected button
const selected = document.querySelector(`[onclick*="${method}"]`);
selected.setAttribute('aria-checked', 'true');
selected.classList.add('selected');
}When to Get Expert Help
Some checkout accessibility issues require more than basic fixes.
Complex Custom Checkouts
Headless implementations, custom checkout builds, and extensive Plus customizations often have deeply integrated accessibility problems. Structural issues in checkout architecture may require significant refactoring.
Third-Party Integration Issues
Payment processors, review widgets, and loyalty programs inject code you don't control. When these have accessibility issues, solutions require vendor coordination or custom workarounds.
Source Code Remediation
TestParty provides expert remediation that addresses checkout accessibility within comprehensive WCAG compliance. Accessibility professionals create code fixes delivered via pull requests. Most e-commerce stores achieve compliance in 14-30 days.
TUSHY achieved full compliance in 30 days with TestParty, including checkout fixes, with a 4-person team. <1% of TestParty customers have been sued while using the platform.
Frequently Asked Questions
How do I test my checkout with a screen reader?
Use VoiceOver (built into Mac—activate with Cmd+F5) or NVDA (free download for Windows). Tab through your entire checkout. Listen for each form field's label announcement. Submit with errors and verify error messages are announced. Complete a test purchase using only keyboard. If anything is confusing or impossible, screen reader users face the same barriers.
What's the most important checkout fix for screen readers?
Proper form labels are the most critical fix. Without labels, screen reader users cannot identify what information each field requires—they hear "edit, blank" with no context. Use the `label` element with a `for` attribute matching the input's `id`. Every form field needs a visible, programmatically associated label.
Do placeholder attributes work as labels for screen readers?
No. Placeholder text is not a substitute for labels. It disappears when users start typing, isn't reliably announced by screen readers, and fails WCAG success criterion 3.3.2. You can use placeholder text for examples ("example@email.com") in addition to proper labels, but never instead of labels.
How do I announce checkout errors to screen readers?
Use `aria-describedby` to associate error messages with form fields. Add `role="alert"` to error messages so they're announced immediately. Include `aria-invalid="true"` on fields with errors. Move focus to the first error field on submission failure. Write specific error messages that explain how to fix the problem.
Do I need to fix checkout accessibility myself?
You can implement these fixes yourself if you have development resources and can verify results with screen reader testing. For complex checkouts or when you lack internal expertise, source code remediation platforms like TestParty provide expert-delivered fixes. TestParty's experts create checkout accessibility fixes as part of comprehensive compliance, typically achieved in 14-30 days.
Will these fixes prevent accessibility lawsuits?
Proper implementation of form labels, error handling, keyboard navigation, and screen reader support addresses the checkout accessibility issues that appear in most e-commerce lawsuits. However, checkout is one part of site-wide compliance. TestParty's comprehensive approach—with <1% of customers sued while using the platform—addresses all WCAG requirements across your entire store.
Related Resources
For more guidance on checkout and e-commerce accessibility:
- E-commerce Checkout Accessibility — Complete checkout WCAG guide
- Best Checkout Accessibility Solution — Solution comparison
- Part 1: How to Fix Common Shopify Accessibility Issues — Beyond checkout
- E-commerce Accessibility Compliance Plan — Implementation framework
- Accessible Checkout Converting Customers — Revenue impact analysis
This article was crafted using a cyborg approach—human expertise enhanced by AI. Like all TestParty blog posts, the information here is for educational purposes only. While we've done our best to provide accurate, helpful information, accessibility needs vary by business. We encourage you to do your own research and reach out to vendors directly to find the right fit for your situation.
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