Shipping Address Form — Live Validation with Jump-to-Field Error Summary

Shipping Address Form — Live Validation Error Summary · Forms · Plain HTML, CSS & JS · Live preview

What's included

Features

Single validateField() function drives both inline errors and the summary panel — no duplicated validation logic
Persistent error summary panel with role="alert" and aria-live="polite" for automatic screen reader announcements
Each summary entry is a clickable jump link that focuses and smooth-scrolls to its field
Live re-validation on input once a field has been marked invalid, without punishing untouched fields
Pluralized "field/fields" summary heading generated dynamically from the error count
Native HTML5 validation attributes (required, pattern, minlength) as the single source of truth
Visual valid/invalid state per field via CSS classes, independent of native browser validation bubbles
novalidate on the form to fully replace default browser validation UI with the custom summary panel

About this UI Snippet

Shipping Address Form with a Live Error Summary Panel

Screenshot of the Shipping Address Form — Live Validation Error Summary snippet rendered live

Most inline-validated forms only show an error message directly beneath the offending field. That works fine when a single field is wrong, but on a multi-field address form with several problems at once, a user has to scroll and hunt through the form to find every red-bordered input. This snippet adds a second layer: a persistent error summary panel that lists every current validation problem in one place, with each item acting as a jump link back to its field.

Two validation moments, one shared function

validateField(input) is the single source of truth for whether a field is valid — it checks required, pattern, and minLength against the input's current value and returns a human-readable message string (or an empty string when valid). Both the per-field blur listener and the whole-form submit handler call this exact same function, so the inline error text under a field and its corresponding line in the summary panel can never disagree about what's wrong.

Why the summary panel is a live region

The panel has role="alert" aria-live="polite", so when it changes content — appearing after a failed submit, or shrinking as fields get fixed — a screen reader announces the update automatically without requiring focus to move there manually. The heading text is generated dynamically (<strong id="errCount"> plus a pluralized "field/fields" suffix) so it reads naturally whether there's one problem or five.

Turning error messages into jump links

Each <li> in the summary contains a <button data-target="fieldName">, not a plain span. Clicking one calls .focus() and .scrollIntoView({ behavior: 'smooth', block: 'center' }) on the matching input — so the summary functions as a table of contents for what's broken, letting a user with many errors fix them one by one from a single stationary list instead of scrolling the whole form repeatedly.

Debounced re-validation while typing

Once a field has been marked invalid, the input event re-validates it on every keystroke so the red border and summary entry clear the instant the user corrects the mistake — but fields that haven't been touched yet, or are already valid, are left alone until blur, avoiding the jarring experience of seeing "required" errors appear while a user is still mid-way through typing their first character.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to explain why sharing one validateField function between the blur handler and the submit handler prevents the inline error and the summary panel from ever drifting out of sync, and to suggest how the same pattern could be extended to async validation (e.g. a ZIP-to-city/state lookup). It's also worth asking for a version that supports per-country field sets, swapping the required/pattern rules based on a country selector.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a shipping address form in HTML, CSS and vanilla JavaScript with live per-field validation and a persistent error summary panel — no external form libraries.

Requirements:
- Fields for full name, address line 1, address line 2 (optional), city, state, ZIP code, and phone, using native HTML5 required, pattern and minlength attributes for validation rules.
- A single JavaScript validation function used both on field blur and on form submit, so inline error text and the summary panel can never disagree about a field's validity.
- A persistent, initially-hidden error summary panel above the submit button with role="alert" and aria-live="polite", listing every current validation problem with a human-readable message.
- Each item in the summary panel must be a clickable element that moves keyboard focus to its corresponding field and smooth-scrolls it into view.
- Once a field has been marked invalid, it should re-validate on every keystroke so its error clears immediately when corrected, without validating untouched fields prematurely.
- On successful submit with no errors, hide the summary panel and show a success status message; on failed submit, populate and reveal the summary panel.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="demo">
  <form class="ship-form" id="shipForm" novalidate>
    <h2>Shipping address</h2>

    <div class="row">
      <label class="field">
        <span>Full name</span>
        <input type="text" name="fullName" data-label="Full name" required minlength="2" />
        <small class="err" data-for="fullName"></small>
      </label>
    </div>

    <div class="row two">
      <label class="field">
        <span>Address line 1</span>
        <input type="text" name="address1" data-label="Address line 1" required minlength="4" />
        <small class="err" data-for="address1"></small>
      </label>
      <label class="field">
        <span>Address line 2 <em>(optional)</em></span>
        <input type="text" name="address2" data-label="Address line 2" />
      </label>
    </div>

    <div class="row three">
      <label class="field">
        <span>City</span>
        <input type="text" name="city" data-label="City" required />
        <small class="err" data-for="city"></small>
      </label>
      <label class="field">
        <span>State</span>
        <input type="text" name="state" data-label="State" required maxlength="2" placeholder="CA" />
        <small class="err" data-for="state"></small>
      </label>
      <label class="field">
        <span>ZIP code</span>
        <input type="text" name="zip" data-label="ZIP code" required pattern="^\d{5}(-\d{4})?$" placeholder="94103" />
        <small class="err" data-for="zip"></small>
      </label>
    </div>

    <div class="row">
      <label class="field">
        <span>Phone</span>
        <input type="tel" name="phone" data-label="Phone" required pattern="^[\d\s()+-]{7,}$" placeholder="(555) 123-4567" />
        <small class="err" data-for="phone"></small>
      </label>
    </div>

    <div class="summary" id="summary" role="alert" aria-live="polite" hidden>
      <div class="summary-head">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/><path d="M12 9v4M12 17h.01"/></svg>
        <span><strong id="errCount">0</strong> field<span id="errPlural">s</span> need attention</span>
      </div>
      <ul id="summaryList"></ul>
    </div>

    <button type="submit" class="submit-btn">Continue to payment</button>
    <p class="status" id="status" role="status" aria-live="polite"></p>
  </form>
</div>

Step by step

How to Use

  1. 1
    Add data-label to every validated inputThe data-label attribute supplies the human-readable field name used in both the inline error and the summary panel.
  2. 2
    Set required, pattern and minlength as neededvalidateField reads these native HTML5 validation attributes directly — no separate validation config object to maintain.
  3. 3
    Add a matching <small class="err" data-for="fieldName">Each validated field needs a sibling error element whose data-for matches the input's name attribute.
  4. 4
    Customize the regex patternsUpdate the pattern attributes on zip and phone in the HTML panel to match your target country's formats.
  5. 5
    Wire up the success pathReplace the statusEl.textContent assignment in the submit handler with your actual form submission or navigation logic.

Real-world uses

Common Use Cases

CART
E-commerce Checkout Flows
Catch address problems before payment, with a summary that lets a shopper fix everything from one place.
SHIP
Shipping Label Generators
Validate carrier-required fields like ZIP format before submitting to a shipping API that would otherwise reject bad data.
Long Multi-Section Forms
Apply the same summary pattern to any long form where scrolling to find every inline error is impractical.
Accessible Government/Finance Forms
Meet accessibility guidelines that call for an error summary at the top of long, high-stakes forms.

Got questions?

Frequently Asked Questions

Inline errors help while filling out a single field; the summary panel helps once several fields have gone wrong and the user needs a consolidated list to work through — the two serve different moments in the same flow, so this form includes both.

No — it is an aria-live region that gets announced without stealing focus. Focus only moves when the user explicitly clicks a summary entry, which is the more predictable and less disorienting pattern.

Add the input with a unique name, a data-label, and the relevant required/pattern/minlength attributes, plus a matching <small class="err" data-for="name"> element — validateAll() picks it up automatically since it queries all input[data-label] elements.

Yes — after an API response with field errors, call the same renderSummary() function with an array of {input, message} pairs built from the server response, reusing the identical summary UI for both client and server errors.

novalidate disables the browser's default validation bubbles so this custom validateField/renderSummary logic is the only validation UI shown, keeping styling and messaging fully consistent across browsers.

The default patterns assume a US-style ZIP and phone format. For international shipping, relax or replace the zip and phone pattern attributes, and consider adding a country selector that swaps which fields are required.