Skip to main content
Back to Blog
WCAG 2.2

WCAG 3.3.7 Redundant Entry: Complete Implementation Guide

Master WCAG 3.3.7 Redundant Entry - Level A criterion. Learn how to eliminate redundant data entry in multi-step forms with autofill, session storage, and practical code examples.

AllAccessible
14 min read
WCAG 2.2FormsUser ExperienceLevel AAutofill
WCAG 3.3.7 Redundant Entry: Complete Implementation Guide

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 name
  • given-name: First name
  • family-name: Last name
  • email: Email address
  • tel: Phone number
  • street-address / address-line1 / address-line2: Address components
  • address-level1: State/Province
  • address-level2: City
  • postal-code: ZIP/Postal code
  • country: Country
  • cc-name: Cardholder name
  • cc-number: Credit card number
  • cc-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

  1. Identify multi-step processes on your site:

    • Checkout flows
    • Registration processes
    • Application forms
    • Booking systems
    • Multi-page surveys
  2. For each process, verify:

    • Is any information requested more than once?
    • Is that information auto-populated or selectable?
    • Are there exceptions (security, validation)?
  3. 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:

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.

Share this article