# No Redundant Entry

**MUST** · **ID:** `forms-redundant-entry` · **Category:** forms
**Source:** [WCAG](https://www.w3.org/WAI/WCAG21/quickref/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/forms-redundant-entry

> MUST: Within one process (checkout, signup, application), never ask for information the user already entered: auto-populate it, or make it selectable — a "same as shipping" checkbox, a dropdown of addresses already given, or the value shown next to the field. WCAG SC 3.3.7 Redundant Entry, Level A. Browser autofill does NOT satisfy it (that is SC 1.3.5); "re-enter your email to confirm" is the classic violation. Exceptions are narrow: essential re-entry, security, or stale data.

Never ask for the same information twice in one process: auto-populate it, or let the user select it

SC 3.3.7 Redundant Entry (Level A, WCAG 2.2) is about a single process — one checkout, one signup, one application — not about sessions. That is what separates it from forms-autocomplete, which is about the autocomplete attribute and browser autofill across visits (SC 1.3.5 Identify Input Purpose). The Understanding document is explicit that browser autofill does not satisfy this criterion: the content itself must carry the value forward. So a step-2 billing form that is an empty duplicate of the step-1 shipping form fails, even if every field is perfectly marked up for autofill. Satisfy it by auto-populating the second form, or by making the value available to select — a "same as shipping" checkbox, a dropdown of addresses already entered, or the value visibly printed next to the field so it can be copied. "Re-enter your email to confirm" is the classic violation: it exists to catch typos, but the previous value is right there on the page, so show it instead of demanding it again. The exceptions are narrow: re-entry that is essential (a password, a memory test), security-required re-entry, or data that has gone stale.

## Bad — do not do this

`forms-redundant-entry-bad`

```tsx
import { useState } from 'react';

const SHIPPING = {
  name: 'Ada Lovelace',
  email: 'ada@example.com',
  street: '12 Marylebone Road',
  city: 'London',
  postcode: 'NW1 5LA',
};

const RETYPED_CHARS = Object.values(SHIPPING).join('').length;

const FIELDS: { key: keyof typeof SHIPPING; label: string; autoComplete: string }[] = [
  { key: 'name', label: 'Full name', autoComplete: 'name' },
  { key: 'email', label: 'Email', autoComplete: 'email' },
  { key: 'street', label: 'Street', autoComplete: 'street-address' },
  { key: 'city', label: 'City', autoComplete: 'address-level2' },
  { key: 'postcode', label: 'Postcode', autoComplete: 'postal-code' },
];

const EMPTY = { name: '', email: '', street: '', city: '', postcode: '' };

export function RedundantEntryBad() {
  const [step, setStep] = useState<1 | 2>(1);
  const [billing, setBilling] = useState(EMPTY);
  const [emailConfirm, setEmailConfirm] = useState('');

  const typed = Object.values(billing).join('').length + emailConfirm.length;

  return (
    <div className="w-full max-w-sm">
      <div className="bg-card border border-border rounded-lg p-4">
        <p className="text-xs text-muted-foreground mb-3">
          Step {step} of 2 — {step === 1 ? 'Shipping' : 'Billing'}
        </p>

        {step === 1 ? (
          <div className="space-y-3">
            {FIELDS.map((f) => (
              <div key={f.key}>
                <label htmlFor={`re-bad-ship-${f.key}`} className="block text-xs font-medium text-foreground mb-1">
                  {f.label}
                </label>
                <input
                  id={`re-bad-ship-${f.key}`}
                  name={f.key}
                  autoComplete={f.autoComplete}
                  defaultValue={SHIPPING[f.key]}
                  className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-card text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
                />
              </div>
            ))}
            <button
              type="button"
              onClick={() => setStep(2)}
              className="w-full px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
            >
              Continue to billing
            </button>
          </div>
        ) : (
          <div className="space-y-3">
            {FIELDS.map((f) => (
              <div key={f.key}>
                <label htmlFor={`re-bad-bill-${f.key}`} className="block text-xs font-medium text-foreground mb-1">
                  Billing {f.label.toLowerCase()}
                </label>
                <input
                  id={`re-bad-bill-${f.key}`}
                  name={`billing-${f.key}`}
                  value={billing[f.key]}
                  onChange={(e) => setBilling((b) => ({ ...b, [f.key]: e.target.value }))}
                  className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-card text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
                />
              </div>
            ))}
            <div>
              <label htmlFor="re-bad-confirm" className="block text-xs font-medium text-foreground mb-1">
                Re-enter your email to confirm
              </label>
              <input
                id="re-bad-confirm"
                name="email-confirm"
                value={emailConfirm}
                onChange={(e) => setEmailConfirm(e.target.value)}
                className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-card text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
              />
            </div>
            <p className="text-xs font-mono text-muted-foreground">
              Re-typed {typed} of {RETYPED_CHARS + SHIPPING.email.length} characters you already entered
            </p>
            <button
              type="button"
              onClick={() => {
                setStep(1);
                setBilling(EMPTY);
                setEmailConfirm('');
              }}
              className="w-full px-4 py-2 text-sm border border-border rounded-lg text-foreground hover:bg-muted focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
            >
              Back to shipping
            </button>
          </div>
        )}
      </div>

      <p className="text-xs text-destructive mt-4">
        Step 2 is an empty duplicate of step 1: same five values, plus an email you must type twice. Nothing is
        auto-populated and nothing is offered for selection — SC 3.3.7 fails.
      </p>
    </div>
  );
}
```

## Good — do this

`forms-redundant-entry-good`

```tsx
import { useState } from 'react';

const SHIPPING = {
  name: 'Ada Lovelace',
  email: 'ada@example.com',
  street: '12 Marylebone Road',
  city: 'London',
  postcode: 'NW1 5LA',
};

const FIELDS: { key: keyof typeof SHIPPING; label: string; autoComplete: string }[] = [
  { key: 'name', label: 'Full name', autoComplete: 'name' },
  { key: 'email', label: 'Email', autoComplete: 'email' },
  { key: 'street', label: 'Street', autoComplete: 'street-address' },
  { key: 'city', label: 'City', autoComplete: 'address-level2' },
  { key: 'postcode', label: 'Postcode', autoComplete: 'postal-code' },
];

const EMPTY = { name: '', email: '', street: '', city: '', postcode: '' };

export function RedundantEntryGood() {
  const [step, setStep] = useState<1 | 2>(1);
  const [shipping, setShipping] = useState(SHIPPING);
  const [sameAsShipping, setSameAsShipping] = useState(true);
  const [billing, setBilling] = useState(EMPTY);

  const values = sameAsShipping ? shipping : billing;

  return (
    <div className="w-full max-w-sm">
      <div className="bg-card border border-border rounded-lg p-4">
        <p className="text-xs text-muted-foreground mb-3">
          Step {step} of 2 — {step === 1 ? 'Shipping' : 'Billing'}
        </p>

        {step === 1 ? (
          <div className="space-y-3">
            {FIELDS.map((f) => (
              <div key={f.key}>
                <label htmlFor={`re-good-ship-${f.key}`} className="block text-xs font-medium text-foreground mb-1">
                  {f.label}
                </label>
                <input
                  id={`re-good-ship-${f.key}`}
                  name={f.key}
                  autoComplete={f.autoComplete}
                  value={shipping[f.key]}
                  onChange={(e) => setShipping((s) => ({ ...s, [f.key]: e.target.value }))}
                  className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-card text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
                />
              </div>
            ))}
            <button
              type="button"
              onClick={() => setStep(2)}
              className="w-full px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
            >
              Continue to billing
            </button>
          </div>
        ) : (
          <div className="space-y-3">
            <label className="flex items-center gap-2 text-sm text-foreground">
              <input
                type="checkbox"
                checked={sameAsShipping}
                onChange={(e) => {
                  const next = e.target.checked;
                  setSameAsShipping(next);
                  // Keep whatever was on screen as the starting point for editing.
                  if (!next) setBilling(shipping);
                }}
                className="h-4 w-4 accent-primary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
              />
              Same as shipping address
            </label>

            {FIELDS.map((f) => (
              <div key={f.key}>
                <label htmlFor={`re-good-bill-${f.key}`} className="block text-xs font-medium text-foreground mb-1">
                  Billing {f.label.toLowerCase()}
                </label>
                <input
                  id={`re-good-bill-${f.key}`}
                  name={`billing-${f.key}`}
                  autoComplete={f.autoComplete}
                  value={values[f.key]}
                  onChange={(e) => {
                    const value = e.target.value;
                    if (sameAsShipping) {
                      setSameAsShipping(false);
                      setBilling({ ...shipping, [f.key]: value });
                    } else {
                      setBilling((b) => ({ ...b, [f.key]: value }));
                    }
                  }}
                  className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-card text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
                />
              </div>
            ))}
            <p className="text-xs font-mono text-muted-foreground">
              Re-typed 0 characters — every value carried forward, all still editable
            </p>
            <button
              type="button"
              onClick={() => setStep(1)}
              className="w-full px-4 py-2 text-sm border border-border rounded-lg text-foreground hover:bg-muted focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
            >
              Back to shipping
            </button>
          </div>
        )}
      </div>

      <p className="text-xs text-success mt-4">
        One checkbox auto-populates all five values, and typing in any field simply takes over. No confirmation
        email field: the address is on screen, so it can be checked instead of re-typed.
      </p>
    </div>
  );
}
```

## References

- [Understanding SC 3.3.7: Redundant Entry](https://www.w3.org/WAI/WCAG22/Understanding/redundant-entry.html)
- [Understanding SC 1.3.5: Identify Input Purpose](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose.html)
