Accessibility pattern · Forms and flows

Address form

Every field carries its autocomplete token, with billing or shipping in front, so a browser fills both addresses in one go, and the country chosen first decides what the postcode is called, how it is checked and whether the state is needed. A PIN code fills in the town and state and says so politely, and Same as billing locks the delivery fields in view with a note, instead of hiding them.

WCAG criteria
7
Keyboard rules
4
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Addresses

Fields are required unless they say optional.

Billing address

House or flat number, building and street.

6 digits, like 560001.

Delivery address

Delivery uses your billing address, so these fields are locked. Untick the box to enter a different one.

House or flat number, building and street.

6 digits, like 560001.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Tab or ShiftTabMoves through the fields. While Same as billing address is ticked, the locked delivery fields are skipped.
Arrow Down or Arrow UpOn the country select, changes the country; the postcode and state fields change to match.
SpaceOn Same as billing address, locks or unlocks the delivery fields, and the note under it says which.
EnterIn a field, saves the form: problems are listed in a summary that takes focus.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
The sixth digit of a known PIN code is typedTown and state filled in from the PIN code. Check them before you go on.
A PIN code that is not on the list is typedWe could not find that PIN code. Enter the town and state yourself.
United Kingdom is chosenBilling address now uses the United Kingdom format: postcode, and county is optional.
Space unticks Same as billing addressSame as billing address, checkbox, not checked. Enter the address the parcel should go to.
A screen reader reads on to a locked delivery fieldAddress line 1, edit text, unavailable, 12 MG Road
Save addresses is pressed with the town missingThere is a problem with your details, group. Billing address: Enter the town or city

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<form class="ap-address-form" data-ap-address-form novalidate aria-labelledby="addr-form-name">
  <div class="ap-address-form__top">
    <h3 class="ap-address-form__name" id="addr-form-name">Addresses</h3>
    <p class="ap-address-form__sub">Fields are required unless they say optional.</p>
  </div>

  <div class="ap-address-form__summary" id="addr-summary" role="group" aria-labelledby="addr-summary-name" aria-describedby="addr-summary-list" tabindex="-1" hidden>
    <h4 class="ap-address-form__summary-name" id="addr-summary-name"><svg class="ap-address-form__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg>There is a problem with your details</h4>
    <ul class="ap-address-form__summary-list" id="addr-summary-list"></ul>
  </div>

  <div class="ap-address-form__body">
    <fieldset class="ap-address-form__group" data-ap-address="b">
      <legend class="ap-address-form__legend">Billing address</legend>
      <div class="ap-address-form__grid">
        <div class="ap-field ap-address-form__field ap-address-form__field--wide">
          <label class="ap-label" for="addr-b-country">Country</label>
          <select class="ap-input ap-address-form__select" id="addr-b-country" name="b-country" autocomplete="billing country" data-ap-part="country">
            <option value="IN" selected>India</option>
            <option value="GB">United Kingdom</option>
            <option value="US">United States</option>
          </select>
        </div>
        <div class="ap-field ap-address-form__field ap-address-form__field--wide">
          <label class="ap-label" for="addr-b-line1">Address line 1</label>
          <p class="ap-hint" id="addr-b-line1-hint">House or flat number, building and street.</p>
          <input class="ap-input" id="addr-b-line1" name="b-line1" type="text" autocomplete="billing address-line1" required aria-describedby="addr-b-line1-hint" data-ap-part="line1" />
        <p class="ap-error ap-address-form__error" id="addr-b-line1-error" hidden><svg class="ap-address-form__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><span class="ap-address-form__vh">Error:</span> <span data-ap-text></span></p>
        </div>
        <div class="ap-field ap-address-form__field ap-address-form__field--wide">
          <label class="ap-label" for="addr-b-line2">Address line 2 <span class="ap-address-form__opt">(optional)</span></label>
          <input class="ap-input" id="addr-b-line2" name="b-line2" type="text" autocomplete="billing address-line2" data-ap-part="line2" />
        </div>
        <div class="ap-field ap-address-form__field ap-address-form__field--wide ap-address-form__post">
          <label class="ap-label" for="addr-b-post" data-ap-post-label>PIN code</label>
          <p class="ap-hint" id="addr-b-post-hint" data-ap-post-hint>6 digits, like 560001.</p>
          <input class="ap-input ap-address-form__short" id="addr-b-post" name="b-post" type="text" inputmode="numeric" autocomplete="billing postal-code" spellcheck="false" required aria-describedby="addr-b-post-hint addr-b-found" data-ap-part="post" />
        <p class="ap-error ap-address-form__error" id="addr-b-post-error" hidden><svg class="ap-address-form__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><span class="ap-address-form__vh">Error:</span> <span data-ap-text></span></p>
          <p class="ap-address-form__found" id="addr-b-found" role="status"><svg class="ap-address-form__icon ap-address-form__icon--ok" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.6 2.6L16 9.6"/></svg><svg class="ap-address-form__icon ap-address-form__icon--info" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 7.5h.01"/></svg><span data-ap-found></span></p>
        </div>
        <div class="ap-field ap-address-form__field">
          <label class="ap-label" for="addr-b-city">Town or city</label>
          <input class="ap-input" id="addr-b-city" name="b-city" type="text" autocomplete="billing address-level2" required data-ap-part="city" />
        <p class="ap-error ap-address-form__error" id="addr-b-city-error" hidden><svg class="ap-address-form__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><span class="ap-address-form__vh">Error:</span> <span data-ap-text></span></p>
        </div>
        <div class="ap-field ap-address-form__field">
          <label class="ap-label" for="addr-b-region"><span data-ap-region-label>State</span> <span class="ap-address-form__opt" data-ap-region-opt hidden>(optional)</span></label>
          <input class="ap-input" id="addr-b-region" name="b-region" type="text" autocomplete="billing address-level1" required data-ap-part="region" />
        <p class="ap-error ap-address-form__error" id="addr-b-region-error" hidden><svg class="ap-address-form__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><span class="ap-address-form__vh">Error:</span> <span data-ap-text></span></p>
        </div>
      </div>
    </fieldset>

    <fieldset class="ap-address-form__group">
      <legend class="ap-address-form__legend">Delivery address</legend>
      <div class="ap-address-form__same">
        <div class="ap-address-form__check">
          <input type="checkbox" id="addr-same" checked aria-describedby="addr-same-note" />
          <label for="addr-same">Same as billing address</label>
        </div>
        <p class="ap-address-form__note" id="addr-same-note" role="status"><svg class="ap-address-form__icon ap-address-form__icon--lock" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg><span data-ap-note>Delivery uses your billing address, so these fields are locked. Untick the box to enter a different one.</span></p>
      </div>
      <fieldset class="ap-address-form__copy" data-ap-address="s" disabled>
        <div class="ap-address-form__grid">
        <div class="ap-field ap-address-form__field ap-address-form__field--wide">
          <label class="ap-label" for="addr-s-country">Country</label>
          <select class="ap-input ap-address-form__select" id="addr-s-country" name="s-country" autocomplete="shipping country" data-ap-part="country">
            <option value="IN" selected>India</option>
            <option value="GB">United Kingdom</option>
            <option value="US">United States</option>
          </select>
        </div>
        <div class="ap-field ap-address-form__field ap-address-form__field--wide">
          <label class="ap-label" for="addr-s-line1">Address line 1</label>
          <p class="ap-hint" id="addr-s-line1-hint">House or flat number, building and street.</p>
          <input class="ap-input" id="addr-s-line1" name="s-line1" type="text" autocomplete="shipping address-line1" required aria-describedby="addr-s-line1-hint" data-ap-part="line1" />
        <p class="ap-error ap-address-form__error" id="addr-s-line1-error" hidden><svg class="ap-address-form__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><span class="ap-address-form__vh">Error:</span> <span data-ap-text></span></p>
        </div>
        <div class="ap-field ap-address-form__field ap-address-form__field--wide">
          <label class="ap-label" for="addr-s-line2">Address line 2 <span class="ap-address-form__opt">(optional)</span></label>
          <input class="ap-input" id="addr-s-line2" name="s-line2" type="text" autocomplete="shipping address-line2" data-ap-part="line2" />
        </div>
        <div class="ap-field ap-address-form__field ap-address-form__field--wide ap-address-form__post">
          <label class="ap-label" for="addr-s-post" data-ap-post-label>PIN code</label>
          <p class="ap-hint" id="addr-s-post-hint" data-ap-post-hint>6 digits, like 560001.</p>
          <input class="ap-input ap-address-form__short" id="addr-s-post" name="s-post" type="text" inputmode="numeric" autocomplete="shipping postal-code" spellcheck="false" required aria-describedby="addr-s-post-hint addr-s-found" data-ap-part="post" />
        <p class="ap-error ap-address-form__error" id="addr-s-post-error" hidden><svg class="ap-address-form__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><span class="ap-address-form__vh">Error:</span> <span data-ap-text></span></p>
          <p class="ap-address-form__found" id="addr-s-found" role="status"><svg class="ap-address-form__icon ap-address-form__icon--ok" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.6 2.6L16 9.6"/></svg><svg class="ap-address-form__icon ap-address-form__icon--info" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 7.5h.01"/></svg><span data-ap-found></span></p>
        </div>
        <div class="ap-field ap-address-form__field">
          <label class="ap-label" for="addr-s-city">Town or city</label>
          <input class="ap-input" id="addr-s-city" name="s-city" type="text" autocomplete="shipping address-level2" required data-ap-part="city" />
        <p class="ap-error ap-address-form__error" id="addr-s-city-error" hidden><svg class="ap-address-form__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><span class="ap-address-form__vh">Error:</span> <span data-ap-text></span></p>
        </div>
        <div class="ap-field ap-address-form__field">
          <label class="ap-label" for="addr-s-region"><span data-ap-region-label>State</span> <span class="ap-address-form__opt" data-ap-region-opt hidden>(optional)</span></label>
          <input class="ap-input" id="addr-s-region" name="s-region" type="text" autocomplete="shipping address-level1" required data-ap-part="region" />
        <p class="ap-error ap-address-form__error" id="addr-s-region-error" hidden><svg class="ap-address-form__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><span class="ap-address-form__vh">Error:</span> <span data-ap-text></span></p>
        </div>
        </div>
      </fieldset>
    </fieldset>
  </div>

  <p class="ap-address-form__vh" role="status" data-ap-live></p>

  <div class="ap-address-form__foot">
    <p class="ap-address-form__status" role="status"><svg class="ap-address-form__icon ap-address-form__icon--ok" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.6 2.6L16 9.6"/></svg><span data-ap-done></span></p>
    <button type="submit" class="ap-btn ap-btn--primary">Save addresses</button>
  </div>
</form>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Each address is a fieldset with a legend, every field has a label, and the PIN code's hint and lookup result are in its aria-describedby.

  • 1.3.5 Identify Input Purpose Level AA

    Every field has its autocomplete token, prefixed with billing or shipping, so a browser can fill the two addresses separately.

  • 3.2.2 On Input Level A

    Choosing a country changes only the labels and hints below it, in place; focus stays on the select and the change is announced.

  • 3.3.1 Error Identification Level A

    A missing or malformed field is marked aria-invalid, its message is written under it, and the summary lists it with the address it belongs to.

  • 3.3.2 Labels or Instructions Level A

    The postcode's label and example follow the country: PIN code with 560001, Postcode with SW1A 1AA, ZIP code with 10001.

  • 3.3.7 Redundant Entry Level A

    Same as billing address copies the first address, so nobody types it twice, and a PIN code fills in the town and state.

  • 4.1.3 Status Messages Level AA

    The lookup result, the country change and the outcome are written into status regions, so they are announced without moving focus.

Usage

When to use it

Use it

  • Checkout, account and delivery forms that take a postal address, especially from more than one country.
  • Forms that need two addresses that are usually the same, like billing and delivery.

Use something else

  • Splitting a house number, a street and a landmark into many required fields: many addresses do not fit the boxes.
  • A map pin or GPS as the only way to give an address: people need to be able to type it.
  • Asking for an address you do not need: a digital product needs at most a country for tax.

Common failures

How it usually goes wrong

  • Autocomplete left off

    Without autocomplete tokens, people type an address their browser already knows, letter by letter. With billing and shipping in front, both addresses fill correctly.

  • A ZIP code box for the whole world

    A five-digit check rejects every Indian PIN code and British postcode. The country decides the label, the example and the check here.

  • Same as billing that hides the fields

    When the delivery fields vanish, people cannot check what will be used, and screen reader users may not know they exist. Here they stay in view, locked, with a note saying why.

  • A lookup that overwrites quietly

    Filling the town without a word leaves people unsure what changed, and replacing something they typed is worse. The lookup here fills only empty or self-filled fields and says so.

  • Disabled fields with no explanation

    A greyed-out field with no reason looks broken. The note beside the checkbox says why the fields are locked and how to unlock them, and is tied to the checkbox.

  • Postcodes rejected for spaces or case

    People type sw1a1aa and 560 001. The check here ignores spaces and case, and only the meaning of the value is tested.

Notes

Building it

  • The postcode comes before the town so the PIN code lookup can fill in what follows. The lookup here is a short fixed list; a real one asks a postal service when the sixth digit arrives.
  • The locked fields sit in a fieldset with the disabled attribute: they stay readable in a screen reader's browse mode but are skipped by Tab and not sent with the form.
  • Choosing a country keeps what was typed and changes only the labels, the hint, inputmode and whether the state is required, so switching by mistake loses nothing.
  • The select has no empty first option: India is chosen by default here. Default to the country most of your customers are in, or the one their connection suggests.
  • Unticking Same as billing address keeps the copied address as a starting point, since most delivery addresses differ only in a line or two.

Sources: HTML: autofill field names · WAI Tutorials: Grouping controls · Understanding SC 1.3.5: Identify Input Purpose

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction