
WCAG 3.3.7 Redundant Entry: Complete Implementation Guide
WCAG 3.3.7 Redundant Entry is a new Level A success criterion introduced in WCAG 2.2. It requires that information previously entered by the user in the same process should not need to be entered again, except where re-entry is essential for security or validation purposes.
This is a Level A requirement, meaning it's fundamental for accessibility compliance under ADA, Section 508, and the European Accessibility Act (EAA).
What Does WCAG 3.3.7 Require?
Official Definition:
Information previously entered by or provided to the user that is required to be entered again in the same process is either:
- auto-populated, or
- available for the user to select
Except where:
- re-entering the information is essential
- the information is required to ensure the security of the content, or
- previously entered information is no longer valid
In Plain English: If a user enters their shipping address on step 2 of checkout, don't make them re-type it on step 4 to confirm their order. Either pre-fill it automatically or let them select it from previously entered options.
Key Points:
- β Applies to multi-step processes (checkout, registration, applications)
- β Information must be auto-populated OR selectable
- β Exceptions for security (re-enter password) and validation
- β Doesn't require data persistence across sessions
Why This Matters
Real-World Impact:
Problem Scenario
User completes 10-field shipping address form (Step 1)
β Reviews cart (Step 2)
β Payment page asks for billing address (Step 3)
β "Same as shipping" checkbox not available
β User must re-type entire address manually
β High frustration, potential errors, cart abandonment
Who This Helps:
- Users with cognitive disabilities: Memory impairments, difficulty retaining information
- Users with motor impairments: Typing is physically challenging
- Mobile users: Typing on small screens is time-consuming
- Older adults: Reduced working memory capacity
- Everyone: Reduces errors, saves time, improves conversion rates
Statistics:
- 69.57% average cart abandonment rate (Baymard Institute)
- 23% abandon due to "too long/complicated checkout process"
- Users make 40% more errors when re-entering data vs. reviewing pre-filled data
Common Failure Patterns
β Failure 1: Billing Address Requires Re-entry
<!-- Step 1: Shipping Address -->
<form id="shipping-form">
<input name="name" value="John Doe">
<input name="address" value="123 Main St">
<input name="city" value="Portland">
<input name="zip" value="97201">
<button>Continue to Payment</button>
</form>
<!-- Step 3: Billing Address (No Pre-fill!) -->
<form id="billing-form">
<input name="billing_name"> <!-- Empty! User must re-type -->
<input name="billing_address"> <!-- Empty! -->
<input name="billing_city"> <!-- Empty! -->
<input name="billing_zip"> <!-- Empty! -->
<button>Continue to Review</button>
</form>
Problem: User already entered this information as shipping address, forcing re-entry violates 3.3.7.
β Failure 2: No "Use Previous" Option in Multi-Page Forms
<!-- Page 1: Emergency Contact -->
<form id="emergency-contact">
<input name="emergency_name" value="Jane Doe">
<input name="emergency_phone" value="555-0123">
<button>Next</button>
</form>
<!-- Page 3: References (Same contact could be reference) -->
<form id="references">
<label>Reference 1 Name</label>
<input name="ref1_name"> <!-- No way to select "Jane Doe" from previous -->
<label>Reference 1 Phone</label>
<input name="ref1_phone"> <!-- Must re-type 555-0123 -->
</form>
Problem: No mechanism to reuse previously entered information.
β Failure 3: Session Expiration Loses Data
<form id="long-application">
<!-- User fills out 50 fields over 20 minutes -->
<!-- Session expires after 15 minutes -->
<!-- User returns to find all data gone -->
<!-- Must start over completely -->
</form>
Problem: Session timeout forcing complete re-entry violates 3.3.7 (unless security-essential).
β Solution 1: "Same as Shipping" Checkbox
The most common and effective solution:
<form id="billing-form">
<div class="form-group">
<label>
<input type="checkbox" id="same-as-shipping">
Billing address same as shipping address
</label>
</div>
<div id="billing-fields">
<label for="billing-name">Name</label>
<input type="text" id="billing-name" name="billing_name">
<label for="billing-address">Address</label>
<input type="text" id="billing-address" name="billing_address">
<label for="billing-city">City</label>
<input type="text" id="billing-city" name="billing_city">
<label for="billing-zip">ZIP Code</label>
<input type="text" id="billing-zip" name="billing_zip">
</div>
</form>
<script>
const sameAsShipping = document.getElementById('same-as-shipping');
const billingFields = document.getElementById('billing-fields');
// Get shipping data from previous step (from sessionStorage, state, etc.)
const shippingData = {
name: sessionStorage.getItem('shipping_name'),
address: sessionStorage.getItem('shipping_address'),
city: sessionStorage.getItem('shipping_city'),
zip: sessionStorage.getItem('shipping_zip')
};
sameAsShipping.addEventListener('change', (e) => {
if (e.target.checked) {
// Auto-fill billing with shipping data
document.getElementById('billing-name').value = shippingData.name;
document.getElementById('billing-address').value = shippingData.address;
document.getElementById('billing-city').value = shippingData.city;
document.getElementById('billing-zip').value = shippingData.zip;
// Optional: disable fields to prevent editing
billingFields.querySelectorAll('input').forEach(input => {
input.setAttribute('readonly', 'readonly');
});
} else {
// Clear fields if unchecked
billingFields.querySelectorAll('input').forEach(input => {
input.value = '';
input.removeAttribute('readonly');
});
}
});
</script>
β Solution 2: Session Storage for Multi-Step Forms
Persist data across steps and page refreshes:
// Save form data as user types
class FormPersistence {
constructor(formId, storageKey) {
this.form = document.getElementById(formId);
this.storageKey = storageKey;
this.init();
}
init() {
// Load saved data on page load
this.loadFormData();
// Save data as user types
this.form.querySelectorAll('input, select, textarea').forEach(field => {
field.addEventListener('input', () => this.saveFormData());
field.addEventListener('change', () => this.saveFormData());
});
// Clear storage on successful submit
this.form.addEventListener('submit', (e) => {
// Only clear if submission successful
if (this.form.checkValidity()) {
this.clearFormData();
}
});
}
saveFormData() {
const formData = new FormData(this.form);
const data = {};
for (let [key, value] of formData.entries()) {
data[key] = value;
}
sessionStorage.setItem(this.storageKey, JSON.stringify(data));
console.log('Form data saved:', data);
}
loadFormData() {
const savedData = sessionStorage.getItem(this.storageKey);
if (savedData) {
const data = JSON.parse(savedData);
Object.keys(data).forEach(key => {
const field = this.form.elements[key];
if (field) {
if (field.type === 'checkbox' || field.type === 'radio') {
field.checked = data[key] === 'on' || data[key] === field.value;
} else {
field.value = data[key];
}
}
});
console.log('Form data loaded:', data);
}
}
clearFormData() {
sessionStorage.removeItem(this.storageKey);
}
}
// Usage
const checkoutForm = new FormPersistence('checkout-form', 'checkout_data');
β Solution 3: Autocomplete Attributes
Leverage browser autofill:
<form id="checkout-form">
<!-- Shipping Address -->
<fieldset>
<legend>Shipping Address</legend>
<label for="shipping-name">Full Name</label>
<input
type="text"
id="shipping-name"
name="shipping_name"
autocomplete="shipping name"
required>
<label for="shipping-address">Street Address</label>
<input
type="text"
id="shipping-address"
name="shipping_address"
autocomplete="shipping address-line1"
required>
<label for="shipping-city">City</label>
<input
type="text"
id="shipping-city"
name="shipping_city"
autocomplete="shipping address-level2"
required>
<label for="shipping-state">State</label>
<select id="shipping-state" name="shipping_state" autocomplete="shipping address-level1">
<option value="">Select State</option>
<option value="CA">California</option>
<option value="NY">New York</option>
</select>
<label for="shipping-zip">ZIP Code</label>
<input
type="text"
id="shipping-zip"
name="shipping_zip"
autocomplete="shipping postal-code"
pattern="[0-9]{5}"
required>
</fieldset>
<!-- Billing Address -->
<fieldset>
<legend>Billing Address</legend>
<label>
<input type="checkbox" id="same-as-shipping">
Same as shipping address
</label>
<!-- Same structure with autocomplete="billing ..." -->
<label for="billing-name">Full Name</label>
<input
type="text"
id="billing-name"
name="billing_name"
autocomplete="billing name">
<label for="billing-address">Street Address</label>
<input
type="text"
id="billing-address"
name="billing_address"
autocomplete="billing address-line1">
<!-- etc. -->
</fieldset>
</form>
Standard Autocomplete Values:
name: Full namegiven-name: First namefamily-name: Last nameemail: Email addresstel: Phone numberstreet-address/address-line1/address-line2: Address componentsaddress-level1: State/Provinceaddress-level2: Citypostal-code: ZIP/Postal codecountry: Countrycc-name: Cardholder namecc-number: Credit card numbercc-exp: Expiration date
β Solution 4: Select from Previously Entered Options
Let users choose from saved addresses/contacts:
<form id="checkout-form">
<div class="form-group">
<label for="address-select">Select Shipping Address</label>
<select id="address-select">
<option value="">Enter new address</option>
<option value="1">Home: 123 Main St, Portland, OR 97201</option>
<option value="2">Work: 456 Oak Ave, Seattle, WA 98101</option>
</select>
</div>
<div id="address-fields">
<label for="address">Street Address</label>
<input type="text" id="address" name="address">
<label for="city">City</label>
<input type="text" id="city" name="city">
<label for="state">State</label>
<input type="text" id="state" name="state">
<label for="zip">ZIP Code</label>
<input type="text" id="zip" name="zip">
</div>
</form>
<script>
const addressSelect = document.getElementById('address-select');
const addressFields = document.getElementById('address-fields');
// Saved addresses (from database, localStorage, etc.)
const savedAddresses = {
'1': {
address: '123 Main St',
city: 'Portland',
state: 'OR',
zip: '97201'
},
'2': {
address: '456 Oak Ave',
city: 'Seattle',
state: 'WA',
zip: '98101'
}
};
addressSelect.addEventListener('change', (e) => {
const selectedId = e.target.value;
if (selectedId && savedAddresses[selectedId]) {
const address = savedAddresses[selectedId];
// Auto-fill fields
document.getElementById('address').value = address.address;
document.getElementById('city').value = address.city;
document.getElementById('state').value = address.state;
document.getElementById('zip').value = address.zip;
} else {
// Clear fields for new address
addressFields.querySelectorAll('input').forEach(input => input.value = '');
}
});
</script>
β Solution 5: Draft Saving for Long Forms
Auto-save progress in lengthy application forms:
class DraftSaver {
constructor(formId, saveInterval = 30000) { // Save every 30 seconds
this.form = document.getElementById(formId);
this.saveInterval = saveInterval;
this.storageKey = `draft_${formId}`;
this.init();
}
init() {
// Load draft on page load
this.loadDraft();
// Auto-save on interval
setInterval(() => this.saveDraft(), this.saveInterval);
// Save on form field changes (debounced)
let saveTimeout;
this.form.addEventListener('input', () => {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => this.saveDraft(), 2000);
});
// Show draft indicator
this.showDraftIndicator();
}
saveDraft() {
const formData = new FormData(this.form);
const data = {
timestamp: new Date().toISOString(),
fields: {}
};
for (let [key, value] of formData.entries()) {
data.fields[key] = value;
}
localStorage.setItem(this.storageKey, JSON.stringify(data));
this.updateDraftIndicator('Draft saved');
}
loadDraft() {
const saved = localStorage.getItem(this.storageKey);
if (saved) {
const data = JSON.parse(saved);
// Ask user if they want to restore
const timeSaved = new Date(data.timestamp).toLocaleString();
if (confirm(`Found saved draft from ${timeSaved}. Restore it?`)) {
Object.keys(data.fields).forEach(key => {
const field = this.form.elements[key];
if (field) {
field.value = data.fields[key];
}
});
this.updateDraftIndicator('Draft restored');
} else {
localStorage.removeItem(this.storageKey);
}
}
}
showDraftIndicator() {
const indicator = document.createElement('div');
indicator.id = 'draft-indicator';
indicator.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
padding: 10px 20px;
background: #4CAF50;
color: white;
border-radius: 4px;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s;
`;
document.body.appendChild(indicator);
}
updateDraftIndicator(message) {
const indicator = document.getElementById('draft-indicator');
if (indicator) {
indicator.textContent = message;
indicator.style.opacity = '1';
setTimeout(() => {
indicator.style.opacity = '0';
}, 3000);
}
}
}
// Usage
const applicationForm = new DraftSaver('job-application-form');
Exceptions to 3.3.7
Exception 1: Security (Password Re-entry)
<!-- Acceptable: Password confirmation for security -->
<form id="signup-form">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
<label for="password-confirm">Confirm Password</label>
<input type="password" id="password-confirm" name="password_confirm" required>
<!-- This re-entry is essential for security -->
</form>
Exception 2: Data Verification
<!-- Acceptable: Email confirmation to catch typos -->
<form id="account-form">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" required>
<label for="email-confirm">Confirm Email Address</label>
<input type="email" id="email-confirm" name="email_confirm" required>
<!-- This re-entry helps verify accuracy -->
</form>
Exception 3: Information No Longer Valid
<!-- Acceptable: Expired or outdated information -->
<form id="renewal-form">
<p>Your credit card ending in 1234 expired on 12/2024.</p>
<p>Please enter new payment information:</p>
<label for="cc-number">New Card Number</label>
<input type="text" id="cc-number" name="cc_number">
<!-- Previous card info no longer valid -->
</form>
Testing for 3.3.7 Compliance
Manual Testing Checklist
-
Identify multi-step processes on your site:
- Checkout flows
- Registration processes
- Application forms
- Booking systems
- Multi-page surveys
-
For each process, verify:
- Is any information requested more than once?
- Is that information auto-populated or selectable?
- Are there exceptions (security, validation)?
-
Test scenarios:
- Complete entire process normally
- Refresh page mid-process (is data retained?)
- Go back to previous step (is data still there?)
- Close browser and return (for saved drafts)
Automated Testing
// Test for redundant form fields
function test337Compliance() {
const forms = document.querySelectorAll('form');
const fieldNames = new Set();
const violations = [];
forms.forEach((form, formIndex) => {
const formFields = form.querySelectorAll('input[name], select[name], textarea[name]');
formFields.forEach(field => {
const fieldName = field.getAttribute('name');
// Skip password fields (security exception)
if (field.type === 'password') return;
// Check if field name already seen in this process
if (fieldNames.has(fieldName)) {
// Check if field is auto-populated or has data-source
const isAutofilled = field.hasAttribute('autocomplete') ||
field.hasAttribute('data-prefill') ||
field.value !== '';
if (!isAutofilled) {
violations.push({
form: formIndex + 1,
fieldName: fieldName,
fieldType: field.type,
issue: 'Field requested again without auto-fill'
});
}
} else {
fieldNames.add(fieldName);
}
});
});
console.log(`\n===== WCAG 3.3.7 Redundant Entry Test =====`);
if (violations.length > 0) {
console.log(`π΄ Found ${violations.length} potential violations:`);
console.table(violations);
} else {
console.log(`β
No redundant entry violations found`);
}
return violations;
}
// Run test
test337Compliance();
Implementation Checklist
E-commerce Checkout
- Billing address "Same as shipping" checkbox
- Save multiple addresses for logged-in users
- Autocomplete attributes on all address fields
- Payment method saved for repeat customers
- Guest checkout email used for order confirmation
User Registration
- Don't ask for email again after initial entry
- Save progress if multi-step registration
- Pre-fill from social login (if used)
- Autocomplete attributes for personal info
Job Applications
- Save draft automatically
- Load previous application data
- Don't re-request resume if already uploaded
- Reference contact info selectable from previous entries
Booking/Reservation Systems
- Passenger info same as account holder option
- Save frequent travelers/guests
- Payment method from previous booking
- Preferences carried over from last reservation
Real-World Examples
β Good Example: Amazon Checkout
Amazon excels at 3.3.7 compliance:
- Saved shipping addresses (select from list)
- "Use shipping address" for billing
- Saved payment methods
- One-click ordering (ultimate anti-redundancy)
β Good Example: Airbnb
Airbnb handles guest information well:
- Save frequent travelers
- Auto-fill from profile
- Previous booking details accessible
- Message thread retains contact info
β Bad Example: Government Forms
Many government forms violate 3.3.7:
- Request same personal info on every page
- No session persistence
- No draft saving
- Force re-entry after timeout
Framework-Specific Implementation
React with Context
import React, { createContext, useState, useContext, useEffect } from 'react';
// Create context for form data
const FormContext = createContext();
export function FormProvider({ children }) {
const [formData, setFormData] = useState(() => {
// Load from sessionStorage on init
const saved = sessionStorage.getItem('checkout_data');
return saved ? JSON.parse(saved) : {};
});
// Save to sessionStorage whenever formData changes
useEffect(() => {
sessionStorage.setItem('checkout_data', JSON.stringify(formData));
}, [formData]);
const updateField = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
return (
<FormContext.Provider value={{ formData, updateField }}>
{children}
</FormContext.Provider>
);
}
// Shipping form component
function ShippingForm() {
const { formData, updateField } = useContext(FormContext);
return (
<form>
<input
value={formData.name || ''}
onChange={(e) => updateField('name', e.target.value)}
placeholder="Full Name"
/>
<input
value={formData.address || ''}
onChange={(e) => updateField('address', e.target.value)}
placeholder="Address"
/>
</form>
);
}
// Billing form component
function BillingForm() {
const { formData, updateField } = useContext(FormContext);
const [sameAsShipping, setSameAsShipping] = useState(false);
const handleSameAsShipping = (checked) => {
setSameAsShipping(checked);
if (checked) {
updateField('billing_name', formData.name);
updateField('billing_address', formData.address);
}
};
return (
<form>
<label>
<input
type="checkbox"
checked={sameAsShipping}
onChange={(e) => handleSameAsShipping(e.target.checked)}
/>
Same as shipping
</label>
<input
value={formData.billing_name || ''}
onChange={(e) => updateField('billing_name', e.target.value)}
placeholder="Billing Name"
disabled={sameAsShipping}
/>
<input
value={formData.billing_address || ''}
onChange={(e) => updateField('billing_address', e.target.value)}
placeholder="Billing Address"
disabled={sameAsShipping}
/>
</form>
);
}
How AllAccessible Helps
AllAccessible automatically detects and suggests fixes for redundant entry violations:
Automatic Detection:
- Scans multi-step processes for duplicate field requests
- Identifies missing autocomplete attributes
- Detects lack of "same as" options
- Checks for session persistence
Recommended Fixes:
- Suggests autocomplete attribute values
- Identifies opportunities for pre-fill
- Recommends draft saving for long forms
- Provides code snippets for "same as" functionality
Start Free Trial - Improve form UX and accessibility - starting at $10/month.
Related Resources
π Master WCAG 2.2: Complete WCAG 2.2 Compliance Guide
Related Success Criteria:
Implementation Guides:
- Form Accessibility Complete Guide
- Autocomplete Attributes Best Practices
- E-commerce Checkout Accessibility
Summary
WCAG 3.3.7 Redundant Entry Requirements:
- β Level A (fundamental requirement)
- β Information entered once must be auto-populated or selectable on re-use
- β Applies to multi-step processes (checkout, registration, applications)
- β Exceptions: Security, validation, invalid data
Quick Implementation:
<!-- Add autocomplete -->
<input name="email" autocomplete="email">
<!-- Add "same as" option -->
<input type="checkbox" id="same-as-shipping">
<!-- Save to sessionStorage -->
<script>
sessionStorage.setItem('shipping_data', JSON.stringify(formData));
</script>
Test by: Completing multi-step processes and verifying no information needs to be re-entered unnecessarily.
Need help eliminating redundant entry across your site? AllAccessible automatically detects and fixes these issues - starting at $10/month.