Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-error" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.3 3h7.4L21 8.3v7.4L15.7 21H8.3L3 15.7V8.3z"/><path d="M12 7.8v5M12 16.2h.01"/></symbol>
  <symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
  <symbol id="i-refresh" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11a8 8 0 0 0-14.7-4.4L4 8"/><path d="M4 4v4h4M4 13a8 8 0 0 0 14.7 4.4L20 16"/><path d="M20 20v-4h-4"/></symbol>
  <symbol id="i-warn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 4.2 2.9 17.3a2 2 0 0 0 1.7 3h14.8a2 2 0 0 0 1.7-3L13.7 4.2a2 2 0 0 0-3.4 0z"/><path d="M12 9.5v4.2M12 16.9h.01"/></symbol>
  <symbol id="i-flag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21V4M5 4h11l-2 4 2 4H5"/></symbol>
  <symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="m8 12.3 2.7 2.7 5.5-5.5"/></symbol>
  <symbol id="i-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9" stroke-dasharray="3 3.2"/></symbol>
  <symbol id="i-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 6 6 6-6 6"/></symbol>
</svg>

<div><span class="lbl">Error summary that links to each field</span>
<form class="xa-form" action="#" novalidate>
  <div class="xa-summary" role="alert" tabindex="-1" aria-labelledby="xa-summary-title" data-error-summary>
    <svg class="xa-icon" aria-hidden="true"><use href="#i-error"/></svg>
    <div>
      <h2 id="xa-summary-title">There are 3 problems with your booking</h2>
      <ul>
        <li><a href="#xa-email">Enter an email address like [email protected]</a></li>
        <li><a href="#xa-date">Choose a workshop date</a></li>
        <li><a href="#xa-seats">Choose between 1 and 8 seats</a></li>
      </ul>
    </div>
  </div>
  <h3>Book a pottery workshop</h3>
  <div class="xa-field"><label for="xa-name">Full name</label><input id="xa-name" name="name" autocomplete="name" value="Ines Duarte"></div>
  <div class="xa-field"><label for="xa-email">Email</label><input id="xa-email" name="email" type="email" autocomplete="email" value="ines.duarte@" aria-invalid="true" aria-describedby="xa-email-error">
    <p class="xa-error" id="xa-email-error"><svg aria-hidden="true"><use href="#i-error"/></svg>Enter an email address like [email protected]</p></div>
  <div class="xa-field"><label for="xa-date">Workshop date</label><select id="xa-date" name="date" aria-invalid="true" aria-describedby="xa-date-error"><option value="">Select a date</option><option>Saturday 11 October, 10:00</option><option>Saturday 18 October, 14:00</option></select>
    <p class="xa-error" id="xa-date-error"><svg aria-hidden="true"><use href="#i-error"/></svg>Choose a workshop date</p></div>
  <div class="xa-field"><label for="xa-seats">Seats</label><input id="xa-seats" name="seats" type="number" inputmode="numeric" value="0" aria-invalid="true" aria-describedby="xa-seats-error">
    <p class="xa-error" id="xa-seats-error"><svg aria-hidden="true"><use href="#i-error"/></svg>Choose between 1 and 8 seats</p></div>
  <button class="xa-submit" type="submit">Book workshop</button>
</form></div>

<div><span class="lbl">With actions</span>
<div class="xa-stack">
  <div class="xa xa-indigo">
    <svg class="xa-icon" aria-hidden="true"><use href="#i-clock"/></svg>
    <div class="xa-body">
      <div class="xa-copy"><p class="xa-title">Your trial ends in 3 days</p><p class="xa-text">Upgrade to keep your 14 projects and 6 teammates on Loom Studio.</p></div>
      <div class="xa-actions"><a class="xa-btn xa-btn-primary" href="#">Upgrade plan</a><button class="xa-btn xa-btn-ghost" type="button">Remind me tomorrow</button></div>
    </div>
  </div>
  <div class="xa xa-neutral">
    <svg class="xa-icon" aria-hidden="true"><use href="#i-refresh"/></svg>
    <div class="xa-body">
      <div class="xa-copy"><p class="xa-title">Version 4.2.1 is ready to install</p><p class="xa-text">Restart to finish updating. Open documents are kept.</p></div>
      <div class="xa-actions"><button class="xa-btn xa-btn-primary" type="button">Restart now</button><button class="xa-btn xa-btn-ghost" type="button">Later</button></div>
    </div>
  </div>
</div></div>

<div><span class="lbl">With a checklist and expandable details</span>
<div class="xa-stack">
  <div class="xa xa-success">
    <svg class="xa-icon" aria-hidden="true"><use href="#i-flag"/></svg>
    <div class="xa-copy">
      <p class="xa-title">Almost ready to publish</p>
      <p class="xa-text">2 of 4 steps done for the Autumn Menu site.</p>
      <div class="xa-meter" role="progressbar" aria-label="Setup progress" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"><span style="width:50%"></span></div>
      <ul class="xa-list">
        <li class="is-done"><svg aria-hidden="true"><use href="#i-check"/></svg><span><span class="sr-only">Done: </span>Connect the domain autumnmenu.co</span></li>
        <li class="is-done"><svg aria-hidden="true"><use href="#i-check"/></svg><span><span class="sr-only">Done: </span>Add opening hours</span></li>
        <li class="is-todo"><svg aria-hidden="true"><use href="#i-circle"/></svg><span><span class="sr-only">To do: </span>Upload a cover photo</span></li>
        <li class="is-todo"><svg aria-hidden="true"><use href="#i-circle"/></svg><span><span class="sr-only">To do: </span>Set a table booking limit</span></li>
      </ul>
    </div>
  </div>
  <div class="xa xa-warning">
    <svg class="xa-icon" aria-hidden="true"><use href="#i-warn"/></svg>
    <div class="xa-copy">
      <p class="xa-title">Import finished with 3 skipped rows</p>
      <p class="xa-text">142 of 145 members were imported from members-2026.csv.</p>
      <details>
        <summary><svg aria-hidden="true"><use href="#i-chevron"/></svg>Show skipped rows</summary>
        <ul class="xa-rows">
          <li><b>Row 18</b><span>Missing email address</span></li>
          <li><b>Row 64</b><span>Duplicate of row 12 ([email protected])</span></li>
          <li><b>Row 131</b><span>Birth date 31/02/1990 is not a real date</span></li>
        </ul>
      </details>
    </div>
  </div>
</div></div>
CSS
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.xa-stack{display:flex;flex-direction:column;gap:14px}
.xa-icon{flex:none;width:20px;height:20px;margin-top:2px}

/* 1. Error summary: sits at the top of the form, links to each field, receives focus after a failed submit */
.xa-form{padding:24px;background:#fff;border:1px solid #e2e8f0;border-radius:16px;box-shadow:0 1px 2px rgba(15,23,42,.05)}
.xa-form h3{margin:0 0 18px;font-size:1.125rem;letter-spacing:-.01em;color:#0f172a}
.xa-summary{display:flex;gap:12px;margin:0 0 22px;padding:16px 18px;border:2px solid #dc2626;border-radius:12px;background:#fef2f2;color:#7f1d1d}
.xa-summary:focus{outline:none}
.xa-summary:focus-visible{outline:3px solid #fca5a5;outline-offset:2px}
.xa-summary .xa-icon{color:#dc2626}
.xa-summary h2{margin:0 0 6px;font-size:1rem;line-height:1.4;color:#7f1d1d}
.xa-summary ul{margin:0;padding-left:18px}
.xa-summary li{margin:2px 0}
.xa-summary a{color:#991b1b;font-weight:600;text-decoration:underline;text-underline-offset:2px;border-radius:3px}
.xa-summary a:hover{text-decoration-thickness:2px}
.xa-summary a:focus-visible{outline:2px solid #7f1d1d;outline-offset:2px}
.xa-field{margin:0 0 16px}
.xa-field label{display:block;margin:0 0 4px;font-size:.875rem;font-weight:600;color:#1e293b}
.xa-field input,.xa-field select{width:100%;height:42px;padding:0 12px;border:1px solid #7c8aa0;border-radius:10px;background:#fff;color:#0f172a;font:inherit;font-size:.9375rem}
.xa-field input:focus-visible,.xa-field select:focus-visible{outline:3px solid #93c5fd;outline-offset:0;border-color:#2563eb}
.xa-field [aria-invalid="true"]{border:2px solid #dc2626}
.xa-field [aria-invalid="true"]:focus-visible{outline-color:#fca5a5;border-color:#dc2626}
.xa-error{display:flex;align-items:center;gap:6px;margin:5px 0 0;font-size:.8125rem;font-weight:600;color:#b91c1c}
.xa-error svg{width:15px;height:15px;flex:none}
.xa-submit{height:42px;padding:0 20px;border:0;border-radius:10px;background:#0f172a;color:#fff;font:inherit;font-weight:600;cursor:pointer}
.xa-submit:focus-visible{outline:3px solid #93c5fd;outline-offset:2px}

/* 2. Alerts with actions */
.xa{display:flex;gap:12px;align-items:flex-start;padding:16px 18px;border:1px solid var(--xa-line);border-radius:14px;background:var(--xa-bg);color:var(--xa-ink)}
.xa-indigo{--xa-bg:#eef2ff;--xa-line:#c7d2fe;--xa-ink:#3730a3;--xa-strong:#1e1b4b;--xa-accent:#4f46e5}
.xa-neutral{--xa-bg:#fff;--xa-line:#e2e8f0;--xa-ink:#475569;--xa-strong:#0f172a;--xa-accent:#0f172a}
.xa-warning{--xa-bg:#fffbeb;--xa-line:#fde68a;--xa-ink:#854d0e;--xa-strong:#713f12;--xa-accent:#d97706}
.xa-success{--xa-bg:#f0fdf4;--xa-line:#bbf7d0;--xa-ink:#166534;--xa-strong:#14532d;--xa-accent:#16a34a}
.xa .xa-icon{color:var(--xa-accent)}
.xa-body{flex:1;min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:12px 20px}
.xa-copy{flex:1 1 280px;min-width:0}
.xa-title{margin:0 0 2px;font-weight:650;color:var(--xa-strong)}
.xa-text{margin:0;font-size:.9375rem}
.xa-actions{display:flex;flex-wrap:wrap;gap:8px}
.xa-btn{display:inline-flex;align-items:center;height:36px;padding:0 14px;border:1px solid transparent;border-radius:9px;font:inherit;font-size:.875rem;font-weight:600;
  text-decoration:none;cursor:pointer;transition:background-color .15s,border-color .15s}
.xa-btn-primary{background:var(--xa-accent);color:#fff}
.xa-btn-primary:hover{filter:brightness(1.1)}
.xa-btn-ghost{background:#fff;border-color:var(--xa-line);color:var(--xa-strong)}
.xa-neutral .xa-btn-ghost{border-color:#cbd5e1}
.xa-btn-ghost:hover{border-color:var(--xa-accent)}
.xa-btn:focus-visible{outline:2px solid var(--xa-accent);outline-offset:2px}

/* 3. Alerts with lists */
.xa-list{margin:10px 0 0;padding:0;list-style:none;font-size:.9375rem}
.xa-list li{display:flex;gap:8px;align-items:flex-start;padding:3px 0}
.xa-list svg{width:18px;height:18px;flex:none;margin-top:2px}
.xa-list .is-done{color:var(--xa-strong)}
.xa-list .is-done svg{color:var(--xa-accent)}
.xa-list .is-todo{color:var(--xa-ink)}
.xa-meter{height:6px;margin:10px 0 0;border-radius:99px;background:#dcfce7;overflow:hidden}
.xa-meter span{display:block;height:100%;border-radius:inherit;background:var(--xa-accent)}
.xa details{margin:10px 0 0}
.xa summary{display:inline-flex;align-items:center;gap:6px;border-radius:6px;color:var(--xa-strong);font-size:.875rem;font-weight:600;cursor:pointer;list-style:none}
.xa summary::-webkit-details-marker{display:none}
.xa summary svg{width:16px;height:16px;transition:transform .2s}
.xa details[open] summary svg{transform:rotate(90deg)}
.xa summary:focus-visible{outline:2px solid var(--xa-strong);outline-offset:3px}
.xa-rows{margin:8px 0 0;padding:10px 14px;border-radius:10px;background:rgba(255,255,255,.7);border:1px solid var(--xa-line);font-size:.875rem;list-style:none}
.xa-rows li{display:flex;gap:12px;padding:4px 0}
.xa-rows li+li{border-top:1px dashed var(--xa-line)}
.xa-rows b{flex:none;width:58px;color:var(--xa-strong)}

@media (max-width:520px){.xa-form{padding:18px}.xa{padding:14px}}
@media (prefers-reduced-motion:reduce){.xa-btn,.xa summary svg{transition:none}}
JavaScript
// Error summary: follow a link straight into its field (the label stays in view), and move focus to
// the summary after a failed submit so it is announced and read first.
document.querySelectorAll('[data-error-summary]').forEach(function (summary) {
  summary.addEventListener('click', function (e) {
    var link = e.target.closest('a[href^="#"]');
    if (!link) return;
    var field = document.getElementById(link.getAttribute('href').slice(1));
    if (!field) return;
    e.preventDefault();
    var label = document.querySelector('label[for="' + field.id + '"]');
    (label || field).scrollIntoView({ block: 'center' });
    field.focus({ preventScroll: true });
  });
  var form = summary.closest('form');
  if (form) form.addEventListener('submit', function (e) { e.preventDefault(); summary.focus(); });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-error" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.3 3h7.4L21 8.3v7.4L15.7 21H8.3L3 15.7V8.3z"/><path d="M12 7.8v5M12 16.2h.01"/></symbol>
  <symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
  <symbol id="i-refresh" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11a8 8 0 0 0-14.7-4.4L4 8"/><path d="M4 4v4h4M4 13a8 8 0 0 0 14.7 4.4L20 16"/><path d="M20 20v-4h-4"/></symbol>
  <symbol id="i-warn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 4.2 2.9 17.3a2 2 0 0 0 1.7 3h14.8a2 2 0 0 0 1.7-3L13.7 4.2a2 2 0 0 0-3.4 0z"/><path d="M12 9.5v4.2M12 16.9h.01"/></symbol>
  <symbol id="i-flag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21V4M5 4h11l-2 4 2 4H5"/></symbol>
  <symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="m8 12.3 2.7 2.7 5.5-5.5"/></symbol>
  <symbol id="i-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9" stroke-dasharray="3 3.2"/></symbol>
  <symbol id="i-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 6 6 6-6 6"/></symbol>
</svg>

<div><span class="lbl">Error summary that links to each field</span>
<form class="card xa-form" action="#" novalidate><div class="card-body">
  <div class="alert xa-summary" role="alert" tabindex="-1" aria-labelledby="xa-summary-title" data-error-summary>
    <svg class="xa-icon" aria-hidden="true"><use href="#i-error"/></svg>
    <div>
      <h2 id="xa-summary-title">There are 3 problems with your booking</h2>
      <ul>
        <li><a class="alert-link" href="#xa-email">Enter an email address like [email protected]</a></li>
        <li><a class="alert-link" href="#xa-date">Choose a workshop date</a></li>
        <li><a class="alert-link" href="#xa-seats">Choose between 1 and 8 seats</a></li>
      </ul>
    </div>
  </div>
  <h3>Book a pottery workshop</h3>
  <div class="mb-3"><label class="form-label" for="xa-name">Full name</label><input class="form-control" id="xa-name" name="name" autocomplete="name" value="Ines Duarte"></div>
  <div class="mb-3"><label class="form-label" for="xa-email">Email</label><input class="form-control is-invalid" id="xa-email" name="email" type="email" autocomplete="email" value="ines.duarte@" aria-invalid="true" aria-describedby="xa-email-error">
    <p class="invalid-feedback mb-0" id="xa-email-error"><svg aria-hidden="true"><use href="#i-error"/></svg>Enter an email address like [email protected]</p></div>
  <div class="mb-3"><label class="form-label" for="xa-date">Workshop date</label><select class="form-select is-invalid" id="xa-date" name="date" aria-invalid="true" aria-describedby="xa-date-error"><option value="">Select a date</option><option>Saturday 11 October, 10:00</option><option>Saturday 18 October, 14:00</option></select>
    <p class="invalid-feedback mb-0" id="xa-date-error"><svg aria-hidden="true"><use href="#i-error"/></svg>Choose a workshop date</p></div>
  <div class="mb-3"><label class="form-label" for="xa-seats">Seats</label><input class="form-control is-invalid" id="xa-seats" name="seats" type="number" inputmode="numeric" value="0" aria-invalid="true" aria-describedby="xa-seats-error">
    <p class="invalid-feedback mb-0" id="xa-seats-error"><svg aria-hidden="true"><use href="#i-error"/></svg>Choose between 1 and 8 seats</p></div>
  <button class="btn xa-submit" type="submit">Book workshop</button>
</div></form></div>

<div><span class="lbl">With actions</span>
<div class="xa-stack">
  <div class="alert xa-indigo">
    <svg class="xa-icon" aria-hidden="true"><use href="#i-clock"/></svg>
    <div class="xa-body">
      <div class="xa-copy"><p class="alert-heading">Your trial ends in 3 days</p><p>Upgrade to keep your 14 projects and 6 teammates on Loom Studio.</p></div>
      <div class="xa-actions"><a class="btn btn-primary" href="#">Upgrade plan</a><button class="btn btn-light" type="button">Remind me tomorrow</button></div>
    </div>
  </div>
  <div class="alert xa-neutral">
    <svg class="xa-icon" aria-hidden="true"><use href="#i-refresh"/></svg>
    <div class="xa-body">
      <div class="xa-copy"><p class="alert-heading">Version 4.2.1 is ready to install</p><p>Restart to finish updating. Open documents are kept.</p></div>
      <div class="xa-actions"><button class="btn btn-primary" type="button">Restart now</button><button class="btn btn-light" type="button">Later</button></div>
    </div>
  </div>
</div></div>

<div><span class="lbl">With a checklist and expandable details</span>
<div class="xa-stack">
  <div class="alert xa-success">
    <svg class="xa-icon" aria-hidden="true"><use href="#i-flag"/></svg>
    <div class="xa-copy">
      <p class="alert-heading">Almost ready to publish</p>
      <p>2 of 4 steps done for the Autumn Menu site.</p>
      <div class="progress" role="progressbar" aria-label="Setup progress" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"><div class="progress-bar" style="width:50%"></div></div>
      <ul class="xa-list">
        <li class="is-done"><svg aria-hidden="true"><use href="#i-check"/></svg><span><span class="visually-hidden">Done: </span>Connect the domain autumnmenu.co</span></li>
        <li class="is-done"><svg aria-hidden="true"><use href="#i-check"/></svg><span><span class="visually-hidden">Done: </span>Add opening hours</span></li>
        <li><svg aria-hidden="true"><use href="#i-circle"/></svg><span><span class="visually-hidden">To do: </span>Upload a cover photo</span></li>
        <li><svg aria-hidden="true"><use href="#i-circle"/></svg><span><span class="visually-hidden">To do: </span>Set a table booking limit</span></li>
      </ul>
    </div>
  </div>
  <div class="alert xa xa-warning">
    <svg class="xa-icon" aria-hidden="true"><use href="#i-warn"/></svg>
    <div class="xa-copy">
      <p class="alert-heading">Import finished with 3 skipped rows</p>
      <p>142 of 145 members were imported from members-2026.csv.</p>
      <details>
        <summary><svg aria-hidden="true"><use href="#i-chevron"/></svg>Show skipped rows</summary>
        <ul class="xa-rows">
          <li><b>Row 18</b><span>Missing email address</span></li>
          <li><b>Row 64</b><span>Duplicate of row 12 ([email protected])</span></li>
          <li><b>Row 131</b><span>Birth date 31/02/1990 is not a real date</span></li>
        </ul>
      </details>
    </div>
  </div>
</div></div>
CSS
/* Built from .alert, .form-control / .form-select with .is-invalid + .invalid-feedback, and .btn. */
.xa-stack{display:flex;flex-direction:column;gap:14px}
.xa-icon{flex:none;width:20px;height:20px;margin-top:2px}
.alert{--bs-alert-margin-bottom:0;--bs-alert-padding-x:18px;--bs-alert-padding-y:16px;--bs-alert-border-radius:14px;display:flex;gap:12px;align-items:flex-start}
.alert .alert-heading{margin:0 0 2px;font-size:1rem;font-weight:650;line-height:1.5;color:var(--xa-strong)}
.alert .alert-heading+p{margin:0;font-size:.9375rem}

/* 1. Error summary */
.xa-form{--bs-card-spacer-x:24px;--bs-card-spacer-y:24px;--bs-card-border-color:#e2e8f0;--bs-card-border-radius:16px;box-shadow:0 1px 2px rgba(15,23,42,.05)}
.xa-form h3{margin:0 0 18px;font-size:1.125rem;font-weight:700;letter-spacing:-.01em;color:#0f172a}
.xa-summary{--bs-alert-bg:#fef2f2;--bs-alert-color:#7f1d1d;--bs-alert-border-color:#dc2626;--bs-alert-link-color:#991b1b;--bs-alert-border-radius:12px;
  margin-bottom:22px;border-width:2px}
.xa-summary:focus{outline:none}
.xa-summary:focus-visible{outline:3px solid #fca5a5;outline-offset:2px}
.xa-summary .xa-icon{color:#dc2626}
.xa-summary h2{margin:0 0 6px;font-size:1rem;font-weight:700;line-height:1.4;color:#7f1d1d}
.xa-summary ul{margin:0;padding-left:18px}
.xa-summary li{margin:2px 0}
.xa-summary .alert-link{font-weight:600;text-decoration:underline;text-underline-offset:2px;border-radius:3px}
.xa-summary .alert-link:hover{text-decoration-thickness:2px}
.xa-summary .alert-link:focus-visible{outline:2px solid #7f1d1d;outline-offset:2px}
.xa-form{--bs-form-invalid-color:#b91c1c;--bs-form-invalid-border-color:#dc2626}
.xa-form .form-label{margin:0 0 4px;font-size:.875rem;font-weight:600;color:#1e293b}
.xa-form .form-control,.xa-form .form-select{height:42px;padding:0 12px;border-color:#7c8aa0;border-radius:10px;font-size:.9375rem;color:#0f172a}
.xa-form .form-select{padding-right:36px}
.xa-form .form-control:focus,.xa-form .form-select:focus{border-color:#2563eb;box-shadow:0 0 0 3px #93c5fd}
.xa-form .form-control.is-invalid,.xa-form .form-select.is-invalid{border-width:2px;border-color:#dc2626;background-image:none;padding-right:12px}
.xa-form .form-select.is-invalid{background-image:var(--bs-form-select-bg-img);padding-right:36px}
.xa-form .is-invalid:focus{border-color:#dc2626;box-shadow:0 0 0 3px #fca5a5}
.xa-form .invalid-feedback{display:flex;align-items:center;gap:6px;margin-top:5px;font-size:.8125rem;font-weight:600}
.xa-form .invalid-feedback svg{width:15px;height:15px;flex:none}
.xa-submit{--bs-btn-bg:#0f172a;--bs-btn-border-color:#0f172a;--bs-btn-color:#fff;--bs-btn-hover-bg:#1e293b;--bs-btn-hover-border-color:#1e293b;--bs-btn-hover-color:#fff;
  --bs-btn-active-bg:#0f172a;--bs-btn-active-color:#fff;--bs-btn-focus-box-shadow:0 0 0 2px #fff,0 0 0 5px #93c5fd;height:42px;padding:0 20px;border-radius:10px;font-weight:600}

/* 2. Alerts with actions */
.xa-indigo{--bs-alert-bg:#eef2ff;--bs-alert-border-color:#c7d2fe;--bs-alert-color:#3730a3;--xa-strong:#1e1b4b;--xa-accent:#4f46e5}
.xa-neutral{--bs-alert-bg:#fff;--bs-alert-border-color:#e2e8f0;--bs-alert-color:#475569;--xa-strong:#0f172a;--xa-accent:#0f172a;--xa-ghost-line:#cbd5e1}
.xa-warning{--bs-alert-bg:#fffbeb;--bs-alert-border-color:#fde68a;--bs-alert-color:#854d0e;--xa-strong:#713f12;--xa-accent:#d97706}
.xa-success{--bs-alert-bg:#f0fdf4;--bs-alert-border-color:#bbf7d0;--bs-alert-color:#166534;--xa-strong:#14532d;--xa-accent:#16a34a}
.alert>.xa-icon{color:var(--xa-accent)}
.xa-body{flex:1;min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:12px 20px}
.xa-copy{flex:1 1 280px;min-width:0}
.xa-actions{display:flex;flex-wrap:wrap;gap:8px}
.xa-actions .btn{--bs-btn-padding-x:14px;--bs-btn-font-size:.875rem;--bs-btn-font-weight:600;--bs-btn-border-radius:9px;
  --bs-btn-focus-box-shadow:0 0 0 2px var(--bs-alert-bg),0 0 0 4px var(--xa-accent);display:inline-flex;align-items:center;height:36px}
.xa-actions .btn-primary{--bs-btn-bg:var(--xa-accent);--bs-btn-border-color:var(--xa-accent);--bs-btn-hover-bg:var(--xa-accent);--bs-btn-hover-border-color:var(--xa-accent);
  --bs-btn-active-bg:var(--xa-accent);--bs-btn-active-border-color:var(--xa-accent)}
.xa-actions .btn-primary:hover{filter:brightness(1.1)}
.xa-actions .btn-light{--bs-btn-bg:#fff;--bs-btn-border-color:var(--xa-ghost-line,var(--bs-alert-border-color));--bs-btn-color:var(--xa-strong);--bs-btn-hover-bg:#fff;
  --bs-btn-hover-border-color:var(--xa-accent);--bs-btn-hover-color:var(--xa-strong);--bs-btn-active-bg:#fff;--bs-btn-active-color:var(--xa-strong)}

/* 3. Alerts with lists */
.xa-list{margin:10px 0 0;padding:0;list-style:none;font-size:.9375rem}
.xa-list li{display:flex;gap:8px;align-items:flex-start;padding:3px 0}
.xa-list svg{width:18px;height:18px;flex:none;margin-top:2px}
.xa-list .is-done{color:var(--xa-strong)}
.xa-list .is-done svg{color:var(--xa-accent)}
.xa-success .progress{--bs-progress-height:6px;--bs-progress-bg:#dcfce7;--bs-progress-bar-bg:var(--xa-accent);--bs-progress-border-radius:99px;margin-top:10px}
.xa details{margin:10px 0 0}
.xa summary{display:inline-flex;align-items:center;gap:6px;border-radius:6px;color:var(--xa-strong);font-size:.875rem;font-weight:600;cursor:pointer;list-style:none}
.xa summary::-webkit-details-marker{display:none}
.xa summary svg{width:16px;height:16px;transition:transform .2s}
.xa details[open] summary svg{transform:rotate(90deg)}
.xa summary:focus-visible{outline:2px solid var(--xa-strong);outline-offset:3px}
.xa-rows{margin:8px 0 0;padding:10px 14px;border-radius:10px;background:rgba(255,255,255,.7);border:1px solid var(--bs-alert-border-color);font-size:.875rem;list-style:none}
.xa-rows li{display:flex;gap:12px;padding:4px 0}
.xa-rows li+li{border-top:1px dashed var(--bs-alert-border-color)}
.xa-rows b{flex:none;width:58px;color:var(--xa-strong)}

@media (max-width:520px){.xa-form{--bs-card-spacer-x:18px;--bs-card-spacer-y:18px}.alert{--bs-alert-padding-x:14px;--bs-alert-padding-y:14px}}
@media (prefers-reduced-motion:reduce){.xa-actions .btn,.xa summary svg{transition:none}}
JavaScript
// Error summary: follow a link straight into its field (the label stays in view), and move focus to
// the summary after a failed submit so it is announced and read first.
document.querySelectorAll('[data-error-summary]').forEach(function (summary) {
  summary.addEventListener('click', function (e) {
    var link = e.target.closest('a[href^="#"]');
    if (!link) return;
    var field = document.getElementById(link.getAttribute('href').slice(1));
    if (!field) return;
    e.preventDefault();
    var label = document.querySelector('label[for="' + field.id + '"]');
    (label || field).scrollIntoView({ block: 'center' });
    field.focus({ preventScroll: true });
  });
  var form = summary.closest('form');
  if (form) form.addEventListener('submit', function (e) { e.preventDefault(); summary.focus(); });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-error" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.3 3h7.4L21 8.3v7.4L15.7 21H8.3L3 15.7V8.3z"/><path d="M12 7.8v5M12 16.2h.01"/></symbol>
  <symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
  <symbol id="i-refresh" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11a8 8 0 0 0-14.7-4.4L4 8"/><path d="M4 4v4h4M4 13a8 8 0 0 0 14.7 4.4L20 16"/><path d="M20 20v-4h-4"/></symbol>
  <symbol id="i-warn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 4.2 2.9 17.3a2 2 0 0 0 1.7 3h14.8a2 2 0 0 0 1.7-3L13.7 4.2a2 2 0 0 0-3.4 0z"/><path d="M12 9.5v4.2M12 16.9h.01"/></symbol>
  <symbol id="i-flag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21V4M5 4h11l-2 4 2 4H5"/></symbol>
  <symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="m8 12.3 2.7 2.7 5.5-5.5"/></symbol>
  <symbol id="i-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9" stroke-dasharray="3 3.2"/></symbol>
  <symbol id="i-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 6 6 6-6 6"/></symbol>
</svg>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Error summary that links to each field</span>
<form class="rounded-2xl border border-[#e2e8f0] bg-white p-6 shadow-[0_1px_2px_rgba(15,23,42,.05)] max-[520px]:p-[18px]" action="#" novalidate>
  <div class="mb-[22px] flex gap-3 rounded-xl border-2 border-[#dc2626] bg-[#fef2f2] px-[18px] py-4 text-[#7f1d1d] focus:outline-none focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#fca5a5]" role="alert" tabindex="-1" aria-labelledby="xa-summary-title" data-error-summary>
    <svg class="mt-0.5 size-5 flex-none text-[#dc2626]" aria-hidden="true"><use href="#i-error"/></svg>
    <div>
      <h2 id="xa-summary-title" class="mb-1.5 text-base leading-[1.4] font-bold text-[#7f1d1d]">There are 3 problems with your booking</h2>
      <ul class="list-disc space-y-0.5 pl-[18px]">
        <li><a class="rounded-[3px] font-semibold text-[#991b1b] underline underline-offset-2 hover:decoration-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#7f1d1d]" href="#xa-email">Enter an email address like [email protected]</a></li>
        <li><a class="rounded-[3px] font-semibold text-[#991b1b] underline underline-offset-2 hover:decoration-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#7f1d1d]" href="#xa-date">Choose a workshop date</a></li>
        <li><a class="rounded-[3px] font-semibold text-[#991b1b] underline underline-offset-2 hover:decoration-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#7f1d1d]" href="#xa-seats">Choose between 1 and 8 seats</a></li>
      </ul>
    </div>
  </div>
  <h3 class="mb-[18px] text-[1.125rem] font-bold tracking-[-.01em] text-[#0f172a]">Book a pottery workshop</h3>
  <div class="mb-4"><label class="mb-1 block text-sm font-semibold text-[#1e293b]" for="xa-name">Full name</label><input class="h-[42px] w-full rounded-[10px] border border-[#7c8aa0] bg-white px-3 text-[.9375rem] text-[#0f172a] focus-visible:border-[#2563eb] focus-visible:outline-3 focus-visible:outline-[#93c5fd]" id="xa-name" name="name" autocomplete="name" value="Ines Duarte"></div>
  <div class="mb-4"><label class="mb-1 block text-sm font-semibold text-[#1e293b]" for="xa-email">Email</label><input class="h-[42px] w-full rounded-[10px] border-2 border-[#dc2626] bg-white px-3 text-[.9375rem] text-[#0f172a] focus-visible:outline-3 focus-visible:outline-[#fca5a5]" id="xa-email" name="email" type="email" autocomplete="email" value="ines.duarte@" aria-invalid="true" aria-describedby="xa-email-error">
    <p class="mt-[5px] flex items-center gap-1.5 text-[.8125rem] font-semibold text-[#b91c1c]" id="xa-email-error"><svg class="size-[15px] flex-none" aria-hidden="true"><use href="#i-error"/></svg>Enter an email address like [email protected]</p></div>
  <div class="mb-4"><label class="mb-1 block text-sm font-semibold text-[#1e293b]" for="xa-date">Workshop date</label><select class="h-[42px] w-full rounded-[10px] border-2 border-[#dc2626] bg-white px-3 text-[.9375rem] text-[#0f172a] focus-visible:outline-3 focus-visible:outline-[#fca5a5]" id="xa-date" name="date" aria-invalid="true" aria-describedby="xa-date-error"><option value="">Select a date</option><option>Saturday 11 October, 10:00</option><option>Saturday 18 October, 14:00</option></select>
    <p class="mt-[5px] flex items-center gap-1.5 text-[.8125rem] font-semibold text-[#b91c1c]" id="xa-date-error"><svg class="size-[15px] flex-none" aria-hidden="true"><use href="#i-error"/></svg>Choose a workshop date</p></div>
  <div class="mb-4"><label class="mb-1 block text-sm font-semibold text-[#1e293b]" for="xa-seats">Seats</label><input class="h-[42px] w-full rounded-[10px] border-2 border-[#dc2626] bg-white px-3 text-[.9375rem] text-[#0f172a] focus-visible:outline-3 focus-visible:outline-[#fca5a5]" id="xa-seats" name="seats" type="number" inputmode="numeric" value="0" aria-invalid="true" aria-describedby="xa-seats-error">
    <p class="mt-[5px] flex items-center gap-1.5 text-[.8125rem] font-semibold text-[#b91c1c]" id="xa-seats-error"><svg class="size-[15px] flex-none" aria-hidden="true"><use href="#i-error"/></svg>Choose between 1 and 8 seats</p></div>
  <button class="h-[42px] cursor-pointer rounded-[10px] bg-[#0f172a] px-5 font-semibold text-white focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#93c5fd]" type="submit">Book workshop</button>
</form></div>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">With actions</span>
<div class="flex flex-col gap-3.5">
  <div class="flex items-start gap-3 rounded-[14px] border border-[#c7d2fe] bg-[#eef2ff] px-[18px] py-4 text-[#3730a3] max-[520px]:p-3.5">
    <svg class="mt-0.5 size-5 flex-none text-[#4f46e5]" aria-hidden="true"><use href="#i-clock"/></svg>
    <div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-5 gap-y-3">
      <div class="min-w-0 flex-[1_1_280px]"><p class="mb-0.5 font-[650] text-[#1e1b4b]">Your trial ends in 3 days</p><p class="text-[.9375rem]">Upgrade to keep your 14 projects and 6 teammates on Loom Studio.</p></div>
      <div class="flex flex-wrap gap-2"><a class="inline-flex h-9 items-center rounded-[9px] border border-transparent bg-[#4f46e5] px-3.5 text-sm font-semibold text-white no-underline transition-colors duration-150 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" href="#">Upgrade plan</a><button class="inline-flex h-9 cursor-pointer items-center rounded-[9px] border border-[#c7d2fe] bg-white px-3.5 text-sm font-semibold text-[#1e1b4b] transition-colors duration-150 hover:border-[#4f46e5] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" type="button">Remind me tomorrow</button></div>
    </div>
  </div>
  <div class="flex items-start gap-3 rounded-[14px] border border-[#e2e8f0] bg-white px-[18px] py-4 text-[#475569] max-[520px]:p-3.5">
    <svg class="mt-0.5 size-5 flex-none text-[#0f172a]" aria-hidden="true"><use href="#i-refresh"/></svg>
    <div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-5 gap-y-3">
      <div class="min-w-0 flex-[1_1_280px]"><p class="mb-0.5 font-[650] text-[#0f172a]">Version 4.2.1 is ready to install</p><p class="text-[.9375rem]">Restart to finish updating. Open documents are kept.</p></div>
      <div class="flex flex-wrap gap-2"><button class="inline-flex h-9 cursor-pointer items-center rounded-[9px] border border-transparent bg-[#0f172a] px-3.5 text-sm font-semibold text-white transition-colors duration-150 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f172a] motion-reduce:transition-none" type="button">Restart now</button><button class="inline-flex h-9 cursor-pointer items-center rounded-[9px] border border-[#cbd5e1] bg-white px-3.5 text-sm font-semibold text-[#0f172a] transition-colors duration-150 hover:border-[#0f172a] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f172a] motion-reduce:transition-none" type="button">Later</button></div>
    </div>
  </div>
</div></div>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">With a checklist and expandable details</span>
<div class="flex flex-col gap-3.5">
  <div class="flex items-start gap-3 rounded-[14px] border border-[#bbf7d0] bg-[#f0fdf4] px-[18px] py-4 text-[#166534] max-[520px]:p-3.5">
    <svg class="mt-0.5 size-5 flex-none text-[#16a34a]" aria-hidden="true"><use href="#i-flag"/></svg>
    <div class="min-w-0 flex-1">
      <p class="mb-0.5 font-[650] text-[#14532d]">Almost ready to publish</p>
      <p class="text-[.9375rem]">2 of 4 steps done for the Autumn Menu site.</p>
      <div class="mt-2.5 h-1.5 overflow-hidden rounded-full bg-[#dcfce7]" role="progressbar" aria-label="Setup progress" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"><span class="block h-full rounded-full bg-[#16a34a]" style="width:50%"></span></div>
      <ul class="mt-2.5 text-[.9375rem]">
        <li class="flex items-start gap-2 py-[3px] text-[#14532d]"><svg class="mt-0.5 size-[18px] flex-none text-[#16a34a]" aria-hidden="true"><use href="#i-check"/></svg><span><span class="sr-only">Done: </span>Connect the domain autumnmenu.co</span></li>
        <li class="flex items-start gap-2 py-[3px] text-[#14532d]"><svg class="mt-0.5 size-[18px] flex-none text-[#16a34a]" aria-hidden="true"><use href="#i-check"/></svg><span><span class="sr-only">Done: </span>Add opening hours</span></li>
        <li class="flex items-start gap-2 py-[3px]"><svg class="mt-0.5 size-[18px] flex-none" aria-hidden="true"><use href="#i-circle"/></svg><span><span class="sr-only">To do: </span>Upload a cover photo</span></li>
        <li class="flex items-start gap-2 py-[3px]"><svg class="mt-0.5 size-[18px] flex-none" aria-hidden="true"><use href="#i-circle"/></svg><span><span class="sr-only">To do: </span>Set a table booking limit</span></li>
      </ul>
    </div>
  </div>
  <div class="flex items-start gap-3 rounded-[14px] border border-[#fde68a] bg-[#fffbeb] px-[18px] py-4 text-[#854d0e] max-[520px]:p-3.5">
    <svg class="mt-0.5 size-5 flex-none text-[#d97706]" aria-hidden="true"><use href="#i-warn"/></svg>
    <div class="min-w-0 flex-1">
      <p class="mb-0.5 font-[650] text-[#713f12]">Import finished with 3 skipped rows</p>
      <p class="text-[.9375rem]">142 of 145 members were imported from members-2026.csv.</p>
      <details class="group mt-2.5">
        <summary class="inline-flex cursor-pointer list-none items-center gap-1.5 rounded-md text-sm font-semibold text-[#713f12] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#713f12] [&::-webkit-details-marker]:hidden"><svg class="size-4 transition-transform duration-200 group-open:rotate-90 motion-reduce:transition-none" aria-hidden="true"><use href="#i-chevron"/></svg>Show skipped rows</summary>
        <ul class="mt-2 rounded-[10px] border border-[#fde68a] bg-white/70 px-3.5 py-2.5 text-sm">
          <li class="flex gap-3 py-1"><b class="w-[58px] flex-none text-[#713f12]">Row 18</b><span>Missing email address</span></li>
          <li class="flex gap-3 border-t border-dashed border-[#fde68a] py-1"><b class="w-[58px] flex-none text-[#713f12]">Row 64</b><span>Duplicate of row 12 ([email protected])</span></li>
          <li class="flex gap-3 border-t border-dashed border-[#fde68a] py-1"><b class="w-[58px] flex-none text-[#713f12]">Row 131</b><span>Birth date 31/02/1990 is not a real date</span></li>
        </ul>
      </details>
    </div>
  </div>
</div></div>
JavaScript
// Error summary: follow a link straight into its field (the label stays in view), and move focus to
// the summary after a failed submit so it is announced and read first.
document.querySelectorAll('[data-error-summary]').forEach(function (summary) {
  summary.addEventListener('click', function (e) {
    var link = e.target.closest('a[href^="#"]');
    if (!link) return;
    var field = document.getElementById(link.getAttribute('href').slice(1));
    if (!field) return;
    e.preventDefault();
    var label = document.querySelector('label[for="' + field.id + '"]');
    (label || field).scrollIntoView({ block: 'center' });
    field.focus({ preventScroll: true });
  });
  var form = summary.closest('form');
  if (form) form.addEventListener('submit', function (e) { e.preventDefault(); summary.focus(); });
});

About this set

Alerts that ask the reader to do something. The error summary sits at the top of a pottery workshop booking form, says how many problems there are and links each one to its field; the fields repeat the message in red and point to it with aria-describedby. A short script makes each link focus its field with the label in view and, after a failed submit, moves focus to the summary so it is read first. This is the pattern GOV.UK and most accessible form guides recommend. The two action alerts pair a message with buttons: a trial reminder with Upgrade plan and Remind me tomorrow, and an update prompt with Restart now and Later; the buttons wrap under the text on narrow screens. The list alerts show a publishing checklist with a progress bar and done or to-do items, and an import warning whose skipped rows open in a native details element, so the alert stays short until someone wants the detail. Use the summary on any form with more than two fields, the action alerts for time-limited prompts, and the list alerts when a result needs detail.

What’s included

  • Error summary that links to each invalid field
  • Fields marked with aria-invalid and aria-describedby
  • Action alerts with primary and secondary buttons that wrap on mobile
  • Checklist alert with a progress bar and hidden Done or To do prefixes
  • Expandable skipped-rows list on native details, no script

Accessibility

The summary has role="alert" and tabindex="-1", so it is announced and can take focus after a failed submit, and each link moves focus straight into its field. Invalid fields carry aria-invalid="true" and aria-describedby pointing at their message. The progress bar is a named progressbar, checklist states are given in visually hidden text, every control has a visible focus outline, and transitions stop under prefers-reduced-motion.

Customise it

In plain CSS the action and list alerts read --xa-bg, --xa-line, --xa-ink, --xa-strong and --xa-accent from the .xa-indigo, .xa-neutral, .xa-success and .xa-warning classes; the summary colours are on .xa-summary. In Bootstrap the same colours are set through --bs-alert-* and --bs-btn-* variables; in Tailwind edit the hex utilities.