Component Library

← Back to gallery

Error summary

Category: Forms · Tags: validation, errors, summary, form, focus

How to use it

Use on any form longer than a couple of fields, so people who submit with mistakes learn how many there are and can jump to each one. Write each message as the fix ("Enter your email address"), use the same words in the summary and next to the field, and keep the inline messages too. For a single field, the inline error alone is enough. Change the ids in every href and aria-describedby together, and replace the demo checks with your real validation (still validating on the server).

Accessibility

When submitting finds problems, a summary box appears above the form and its heading (tabindex="-1") receives focus, so screen readers read "There is a problem" and the list right away. Each error is a real <a href="#…"> to the field's id, and the script also moves keyboard focus into that field, scrolling its label into view. The fields get aria-invalid="true" and their inline message is added to aria-describedby; errors are marked with words and a thick border, not colour alone, and links and fields have a 2px focus outline. Text is #0f172a on #ffffff (17.85:1), summary links #b91c1c on #ffffff (6.47:1) and #7f1d1d on #ffffff when hovered (10.02:1), and inline errors #b91c1c on #ffffff (6.47:1).

Source (copy-ready)

<form class="dcl-errsum" novalidate>
  <div class="dcl-errsum__box" hidden>
    <h2 class="dcl-errsum__heading" id="dcl-errsum-heading" tabindex="-1">There is a problem</h2>
    <ul class="dcl-errsum__list">
      <li class="dcl-errsum__entry" data-for="dcl-errsum-name" hidden><a class="dcl-errsum__link" href="#dcl-errsum-name">Enter your full name</a></li>
      <li class="dcl-errsum__entry" data-for="dcl-errsum-email" hidden><a class="dcl-errsum__link" href="#dcl-errsum-email">Enter an email address in the format name@example.com</a></li>
    </ul>
  </div>
  <div class="dcl-errsum__field">
    <label class="dcl-errsum__label" for="dcl-errsum-name">Full name</label>
    <p class="dcl-errsum__error" id="dcl-errsum-name-error" hidden>Error: Enter your full name</p>
    <input class="dcl-errsum__input" id="dcl-errsum-name" name="name" type="text" autocomplete="name">
  </div>
  <div class="dcl-errsum__field">
    <label class="dcl-errsum__label" for="dcl-errsum-email">Email address</label>
    <p class="dcl-errsum__error" id="dcl-errsum-email-error" hidden>Error: Enter an email address in the format name@example.com</p>
    <input class="dcl-errsum__input" id="dcl-errsum-email" name="email" type="email" autocomplete="email">
  </div>
  <button type="submit" class="dcl-errsum__submit">Send request</button>
  <p class="dcl-errsum__status" role="status"></p>
</form>
<style>
.dcl-errsum { display: flex; flex-direction: column; gap: 1rem; max-width: 26rem; font: inherit; color: #0f172a; background-color: #ffffff; }
.dcl-errsum__box { padding: 1rem; border: 4px solid #b91c1c; border-radius: 0.375rem; color: #0f172a; background-color: #ffffff; }
.dcl-errsum__box[hidden] { display: none; }
.dcl-errsum__heading { margin: 0 0 0.5rem; font-size: 1.15rem; color: #0f172a; background-color: #ffffff; }
.dcl-errsum__heading:focus-visible { outline: 2px solid #1d4ed8; outline-offset: 2px; }
.dcl-errsum__list { margin: 0; padding-left: 1.25rem; }
.dcl-errsum__entry { margin: 0.25rem 0; }
.dcl-errsum__entry[hidden] { display: none; }
.dcl-errsum__link { font-weight: 600; text-decoration: underline; overflow-wrap: anywhere; color: #b91c1c; background-color: #ffffff; }
.dcl-errsum__link:hover { color: #7f1d1d; background-color: #ffffff; }
.dcl-errsum__link:focus-visible { outline: 2px solid #0f172a; outline-offset: 2px; }
.dcl-errsum__field { display: flex; flex-direction: column; gap: 0.3rem; }
.dcl-errsum__label { font-weight: 600; color: #0f172a; background-color: #ffffff; }
.dcl-errsum__error { margin: 0; font-weight: 600; color: #b91c1c; background-color: #ffffff; }
.dcl-errsum__input { min-width: 0; min-height: 2.75rem; padding: 0.5rem 0.7rem; border: 1px solid #64748b; border-radius: 0.375rem; font: inherit; color: #0f172a; background-color: #ffffff; }
.dcl-errsum__input[aria-invalid="true"] { border: 3px solid #b91c1c; }
.dcl-errsum__input:focus-visible { outline: 2px solid #1d4ed8; outline-offset: 1px; }
.dcl-errsum__submit { align-self: flex-start; min-height: 2.75rem; padding: 0 1.25rem; border: none; border-radius: 0.5rem; cursor: pointer; font: inherit; font-weight: 600; color: #ffffff; background-color: #1d4ed8; }
.dcl-errsum__submit:hover { color: #ffffff; background-color: #1e40af; }
.dcl-errsum__submit:focus-visible { outline: 2px solid #0f172a; outline-offset: 2px; }
.dcl-errsum__status { min-height: 1.5em; margin: 0; color: #0f172a; background-color: #ffffff; }
</style>
<script>
(function () {
  document.querySelectorAll(".dcl-errsum").forEach(function (root) {
    if (root.dataset.dclBound) { return; }
    root.dataset.dclBound = "true";
    var box = root.querySelector(".dcl-errsum__box");
    var heading = root.querySelector(".dcl-errsum__heading");
    var status = root.querySelector(".dcl-errsum__status");
    var checks = {
      "dcl-errsum-name": function (value) { return value.trim() !== ""; },
      "dcl-errsum-email": function (value) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()); }
    };
    function byId(id) { return root.querySelector("[id=\"" + id + "\"]"); }
    root.addEventListener("submit", function (event) {
      event.preventDefault();
      var failed = 0;
      Object.keys(checks).forEach(function (id) {
        var input = byId(id);
        if (!input) { return; }
        var ok = checks[id](input.value);
        var error = byId(id + "-error");
        var entry = root.querySelector(".dcl-errsum__entry[data-for=\"" + id + "\"]");
        if (error) { error.hidden = ok; }
        if (entry) { entry.hidden = ok; }
        if (ok) {
          input.removeAttribute("aria-invalid");
          input.removeAttribute("aria-describedby");
        } else {
          failed += 1;
          input.setAttribute("aria-invalid", "true");
          if (error) { input.setAttribute("aria-describedby", error.id); }
        }
      });
      if (box) { box.hidden = failed === 0; }
      if (status) { status.textContent = failed === 0 ? "Request sent." : ""; }
      if (failed && heading) { heading.focus(); }
    });
    root.querySelectorAll(".dcl-errsum__link").forEach(function (link) {
      link.addEventListener("click", function (event) {
        var target = byId((link.getAttribute("href") || "").slice(1));
        if (!target) { return; }
        event.preventDefault();
        var label = root.querySelector("label[for=\"" + target.id + "\"]");
        (label || target).scrollIntoView({ block: "center" });
        target.focus();
      });
    });
  });
})();
</script>

Was this useful? Send feedback.