Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-cal" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="5" width="17" height="15.5" rx="2.5"/><path d="M3.5 10h17M8 3v4M16 3v4"/></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-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s-7-6.2-7-11.5a7 7 0 0 1 14 0C19 14.8 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.5"/></symbol>
  <symbol id="i-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></symbol>
  <symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/></symbol>
  <symbol id="i-undo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></symbol>
</svg>

<div class="col an" data-an-invites>
<div><span class="lbl">Invitations with Accept and Decline</span>
<ul class="an-list">
  <li class="an-card" data-an-invite data-accepted="You joined Harbor Design System as an Editor." data-declined="Invitation declined. Dana won’t be notified.">
    <span class="an-av c1" aria-hidden="true">DW</span>
    <div class="an-body"><b>Dana Whitfield</b> invited you to join the <b>Harbor Design System</b> workspace as an Editor.
      <span class="an-time">2 hours ago</span>
      <div class="an-actions"><button class="an-btn primary" type="button" data-an-accept>Accept</button><button class="an-btn" type="button" data-an-decline>Decline</button></div>
      <p class="an-result" hidden tabindex="-1" data-an-result></p></div>
  </li>
  <li class="an-card" data-an-invite data-accepted="Added to your calendar for Thursday, 23 October." data-declined="You declined Design critique. Ravi will see your reply.">
    <span class="an-av cal" aria-hidden="true"><svg><use href="#i-cal"/></svg></span>
    <div class="an-body"><b>Ravi Shah</b> invited you to <b>Design critique: checkout flow</b>
      <div class="an-detail"><span><svg aria-hidden="true"><use href="#i-clock"/></svg>Thu 23 Oct, 14:00–14:45</span><span><svg aria-hidden="true"><use href="#i-pin"/></svg>Room 4B and video call</span></div>
      <div class="an-actions"><button class="an-btn primary" type="button" data-an-accept>Accept</button><button class="an-btn" type="button" data-an-decline>Decline</button></div>
      <p class="an-result" hidden tabindex="-1" data-an-result></p></div>
  </li>
  <li class="an-card" data-an-invite data-accepted="Mateo can now view Q4 forecast.xlsx." data-declined="Request denied. Mateo will get an email.">
    <span class="an-av c2" aria-hidden="true">MR</span>
    <div class="an-body"><b>Mateo Ruiz</b> requested view access to <b>Q4 forecast.xlsx</b>
      <span class="an-time">Yesterday at 16:20</span>
      <div class="an-actions"><button class="an-btn primary" type="button" data-an-accept>Approve</button><button class="an-btn" type="button" data-an-decline>Deny</button></div>
      <p class="an-result" hidden tabindex="-1" data-an-result></p></div>
  </li>
</ul>
<p class="sr-only" role="status" data-an-status></p>
</div>
</div>

<div class="col an">
<div><span class="lbl">Undo toast with a countdown</span>
<div class="an-stage" data-an-undo-demo>
  <button class="an-btn" type="button" data-an-delete>Delete conversation</button>
  <div class="an-stage" data-an-undo-region>
    <div class="an-snack" data-an-snack>
      <p><b>Conversation deleted.</b> <span data-an-snack-text>“Q4 launch plan” with Ana Silva</span></p>
      <button class="an-undo" type="button" data-an-undo><svg width="15" height="15" aria-hidden="true"><use href="#i-undo"/></svg>Undo <span class="an-sec" data-an-sec>10s</span></button>
      <button class="an-x" type="button" aria-label="Dismiss" data-an-snack-close><svg aria-hidden="true"><use href="#i-x"/></svg></button>
      <span class="an-bar" aria-hidden="true" data-an-bar></span>
    </div>
  </div>
  <p class="sr-only" role="status" data-an-undo-status></p>
</div></div>

<div><span class="lbl">Upload progress toast</span>
<section class="an-up" aria-labelledby="an-up-title" data-an-upload>
  <div class="an-up-head"><h3 id="an-up-title" data-an-up-title>Uploading 3 files</h3>
    <button class="an-x" type="button" aria-label="Cancel uploads" data-an-cancel><svg aria-hidden="true"><use href="#i-x"/></svg></button></div>
  <ul class="an-files">
    <li class="an-file" data-an-file data-rate="9" data-start="100">
      <span class="an-ficon" aria-hidden="true">PNG</span><span class="an-fname" id="an-f1">homepage-hero.png</span><span class="an-pct" data-an-pct>100%</span>
      <div class="an-track" role="progressbar" aria-labelledby="an-f1" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"><div class="an-fill" style="width:100%"></div></div>
    </li>
    <li class="an-file" data-an-file data-rate="6" data-start="62">
      <span class="an-ficon" aria-hidden="true">PDF</span><span class="an-fname" id="an-f2">brand-guidelines.pdf</span><span class="an-pct" data-an-pct>62%</span>
      <div class="an-track" role="progressbar" aria-labelledby="an-f2" aria-valuemin="0" aria-valuemax="100" aria-valuenow="62"><div class="an-fill" style="width:62%"></div></div>
    </li>
    <li class="an-file" data-an-file data-rate="3" data-start="18">
      <span class="an-ficon" aria-hidden="true">MP4</span><span class="an-fname" id="an-f3">launch-teaser.mp4</span><span class="an-pct" data-an-pct>18%</span>
      <div class="an-track" role="progressbar" aria-labelledby="an-f3" aria-valuemin="0" aria-valuemax="100" aria-valuenow="18"><div class="an-fill" style="width:18%"></div></div>
    </li>
  </ul>
  <div class="an-up-foot"><span data-an-up-meta>About 20 seconds left</span><a class="an-link" href="#" hidden data-an-view>View files</a><button class="an-btn" type="button" hidden data-an-again>Upload again</button></div>
  <p class="sr-only" role="status" data-an-up-status></p>
</section></div>
</div>
CSS
/* Actionable notifications: invitations answered in place, an undo toast with a visible countdown, and an upload toast.
   Every result is announced through a polite live region, and focus moves to the result line after Accept or Decline. */
.an{--an-brand:#4f46e5;--an-ink:#0f172a;--an-text:#475569;--an-muted:#5b6474;--an-line:#e2e8f0;color:var(--an-text);font-size:.9rem;line-height:1.45}
.an [hidden]{display:none!important}
.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}
.an-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:36px;padding:0 14px;border:1px solid #cbd5e1;border-radius:10px;background:#fff;color:var(--an-ink);
  font:inherit;font-size:.8125rem;font-weight:650;text-decoration:none;cursor:pointer}
.an-btn:hover{background:#f1f5f9}
.an-btn.primary{border-color:var(--an-brand);background:var(--an-brand);color:#fff}
.an-btn.primary:hover{background:#4338ca}
.an-btn svg{width:16px;height:16px}
.an-btn:focus-visible,.an-x:focus-visible,.an-undo:focus-visible,.an-link:focus-visible,.an-result:focus-visible{outline:2px solid var(--an-brand);outline-offset:2px}
/* 1. Invitations */
.an-list{margin:0;padding:0;list-style:none;background:#fff;border:1px solid var(--an-line);border-radius:18px;box-shadow:0 1px 2px rgba(15,23,42,.05),0 20px 40px -30px rgba(15,23,42,.3)}
.an-card{display:flex;gap:14px;padding:18px 20px}
.an-card+.an-card{border-top:1px solid var(--an-line)}
.an-av{flex:none;display:grid;place-items:center;width:42px;height:42px;border-radius:50%;font-size:.8125rem;font-weight:700}
.an-av svg{width:20px;height:20px}
.an-av.c1{background:#ddd6fe;color:#4c1d95}.an-av.c2{background:#fde68a;color:#78350f}.an-av.cal{border-radius:12px;background:#e0f2fe;color:#075985}
.an-body{flex:1;min-width:0}
.an-body b{color:var(--an-ink);font-weight:650}
.an-detail{display:flex;flex-wrap:wrap;gap:6px 12px;margin:8px 0 0;padding:10px 12px;border-radius:10px;background:#f8fafc;border:1px solid var(--an-line);font-size:.8125rem;color:#334155}
.an-detail span{display:inline-flex;align-items:center;gap:6px}
.an-detail svg{width:14px;height:14px;color:var(--an-muted)}
.an-time{display:block;margin-top:6px;font-size:.75rem;color:var(--an-muted)}
.an-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
.an-result{display:flex;align-items:center;gap:8px;margin:12px 0 0;padding:8px 12px;border-radius:10px;font-size:.8125rem;font-weight:600}
.an-result svg{flex:none;width:16px;height:16px}
.an-result[data-tone="ok"]{background:#f0fdf4;color:#166534}
.an-result[data-tone="no"]{background:#f1f5f9;color:#334155}
/* 2. Undo toast */
.an-stage{display:flex;flex-direction:column;align-items:flex-start;gap:14px}
.an-snack{position:relative;display:flex;align-items:center;gap:12px;width:100%;max-width:420px;padding:12px 10px 12px 16px;overflow:hidden;border-radius:14px;background:#0f172a;color:#cbd5e1;
  box-shadow:0 20px 40px -18px rgba(15,23,42,.55);animation:an-in .25s cubic-bezier(.2,.8,.2,1)}
.an-snack p{flex:1;min-width:0;margin:0}
.an-snack b{color:#fff;font-weight:600}
.an-undo{display:inline-flex;align-items:center;gap:8px;height:34px;padding:0 10px 0 12px;border:0;border-radius:9px;background:rgba(165,180,252,.14);color:#c7d2fe;font:inherit;font-size:.8125rem;font-weight:700;cursor:pointer}
.an-undo:hover{background:rgba(165,180,252,.24)}
.an-undo:focus-visible{outline-color:#a5b4fc}
.an-sec{min-width:30px;padding:1px 6px;border-radius:6px;background:rgba(255,255,255,.12);color:#e2e8f0;font-variant-numeric:tabular-nums;font-weight:600;text-align:center}
.an-x{flex:none;display:grid;place-items:center;width:30px;height:30px;padding:0;border:0;border-radius:8px;background:transparent;color:#cbd5e1;cursor:pointer}
.an-x:hover{background:rgba(255,255,255,.1);color:#fff}
.an-x svg{width:14px;height:14px}
.an-snack .an-x:focus-visible{outline-color:#a5b4fc}
.an-bar{position:absolute;left:0;bottom:0;height:3px;width:100%;background:#818cf8;transform-origin:left;transform:scaleX(var(--an-left,1));transition:transform 1s linear}
/* 3. Upload toast */
.an-up{width:100%;max-width:420px;background:#fff;border:1px solid var(--an-line);border-radius:16px;box-shadow:0 20px 40px -20px rgba(15,23,42,.38),0 2px 6px rgba(15,23,42,.06)}
.an-up-head{display:flex;align-items:center;gap:10px;padding:14px 12px 12px 16px;border-bottom:1px solid var(--an-line)}
.an-up-head h3{flex:1;margin:0;font-size:.9375rem;color:var(--an-ink)}
.an-up-head .an-x{color:#64748b}
.an-up-head .an-x:hover{background:#f1f5f9;color:var(--an-ink)}
.an-files{margin:0;padding:6px 16px 10px;list-style:none}
.an-file{display:grid;grid-template-columns:34px 1fr auto;align-items:center;gap:4px 12px;padding:10px 0}
.an-file+.an-file{border-top:1px dashed var(--an-line)}
.an-ficon{grid-row:span 2;display:grid;place-items:center;width:34px;height:34px;border-radius:9px;background:#eef2ff;color:var(--an-brand);font-size:.625rem;font-weight:800;letter-spacing:.03em}
.an-fname{min-width:0;overflow:hidden;color:var(--an-ink);font-weight:600;text-overflow:ellipsis;white-space:nowrap}
.an-pct{font-size:.75rem;font-weight:600;font-variant-numeric:tabular-nums;color:var(--an-muted)}
.an-file[data-done] .an-pct{color:#15803d}
.an-track{grid-column:2 / -1;height:6px;border-radius:6px;background:#e2e8f0;overflow:hidden}
.an-fill{height:100%;width:0;border-radius:inherit;background:var(--an-brand);transition:width .25s linear}
.an-file[data-done] .an-fill{background:#16a34a}
.an-up-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:10px 16px 14px;font-size:.8125rem;color:var(--an-muted)}
.an-link{color:#4338ca;font-weight:650;text-decoration:none}
.an-link:hover{text-decoration:underline}
@keyframes an-in{from{opacity:0;transform:translateY(10px)}}
@media (max-width:480px){.an-card{padding:16px}.an-av{width:36px;height:36px}}
@media (prefers-reduced-motion:reduce){.an-snack{animation:none}.an-bar,.an-fill{transition:none}}
JavaScript
(function () {
  // 1. Invitations: answer in place, then move focus to the result so keyboard users are not dropped.
  document.querySelectorAll('[data-an-invite]').forEach(function (card) {
    var result = card.querySelector('[data-an-result]');
    var actions = card.querySelector('[data-an-accept]').parentNode;
    var status = card.closest('[data-an-invites]').querySelector('[data-an-status]');
    function answer(ok) {
      result.textContent = ok ? card.dataset.accepted : card.dataset.declined;
      result.dataset.tone = ok ? 'ok' : 'no';
      result.hidden = false; actions.hidden = true;
      result.focus();
      if (status) status.textContent = result.textContent;
    }
    card.querySelector('[data-an-accept]').addEventListener('click', function () { answer(true); });
    card.querySelector('[data-an-decline]').addEventListener('click', function () { answer(false); });
  });

  // 2. Undo toast: a one-second countdown that pauses while the pointer or keyboard focus is on the toast.
  var demo = document.querySelector('[data-an-undo-demo]');
  if (demo) {
    var snack = demo.querySelector('[data-an-snack]'), sec = demo.querySelector('[data-an-sec]'), bar = demo.querySelector('[data-an-bar]');
    var text = demo.querySelector('[data-an-snack-text]'), undo = demo.querySelector('[data-an-undo]');
    var say = demo.querySelector('[data-an-undo-status]');
    var TOTAL = 10, left = TOTAL, timer = null, paused = false;
    function render() { sec.textContent = left + 's'; bar.style.setProperty('--an-left', left / TOTAL); }
    function hide() { clearInterval(timer); snack.hidden = true; }
    function start() {
      left = TOTAL; render(); snack.hidden = false; undo.hidden = false;
      text.textContent = '“Q4 launch plan” with Ana Silva';
      snack.querySelector('b').textContent = 'Conversation deleted.';
      clearInterval(timer);
      say.textContent = 'Conversation deleted. Undo is available for ' + TOTAL + ' seconds.';
      timer = setInterval(function () { if (paused) return; left -= 1; render(); if (left <= 0) hide(); }, 1000);
    }
    ['mouseenter', 'focusin'].forEach(function (ev) { snack.addEventListener(ev, function () { paused = true; }); });
    snack.addEventListener('mouseleave', function () { paused = snack.contains(document.activeElement); });
    snack.addEventListener('focusout', function (e) { if (!snack.contains(e.relatedTarget)) paused = snack.matches(':hover'); });
    undo.addEventListener('click', function () {
      clearInterval(timer);
      snack.querySelector('b').textContent = 'Conversation restored.';
      text.textContent = 'It’s back in your inbox.';
      undo.hidden = true;
      say.textContent = 'Conversation restored.';
      demo.querySelector('[data-an-snack-close]').focus();
      timer = setTimeout(hide, 4000);
    });
    demo.querySelector('[data-an-snack-close]').addEventListener('click', function () { hide(); demo.querySelector('[data-an-delete]').focus(); });
    demo.querySelector('[data-an-delete]').addEventListener('click', start);
    start();
  }

  // 3. Upload toast: simulated progress. Replace tick() with your upload's progress events.
  var up = document.querySelector('[data-an-upload]');
  if (up) {
    var files = [].slice.call(up.querySelectorAll('[data-an-file]')), loop = null;
    var title = up.querySelector('[data-an-up-title]'), meta = up.querySelector('[data-an-up-meta]'), status2 = up.querySelector('[data-an-up-status]');
    var view = up.querySelector('[data-an-view]'), again = up.querySelector('[data-an-again]'), cancel = up.querySelector('[data-an-cancel]');
    function set(f, v) {
      v = Math.min(100, Math.round(v)); f.dataset.value = v;
      f.querySelector('[data-an-pct]').textContent = v + '%';
      f.querySelector('[role="progressbar"]').setAttribute('aria-valuenow', v);
      f.querySelector('[role="progressbar"] > div').style.width = v + '%';
      if (v >= 100) f.setAttribute('data-done', ''); else f.removeAttribute('data-done');
    }
    function finish(msg, done) {
      clearInterval(loop);
      title.textContent = msg; status2.textContent = msg;
      meta.hidden = true; view.hidden = !done; again.hidden = false; cancel.hidden = true;
    }
    function tick() {
      var remaining = 0;
      files.forEach(function (f) { var v = +f.dataset.value; if (v < 100) { set(f, v + +f.dataset.rate); } if (+f.dataset.value < 100) remaining++; });
      if (!remaining) return finish('3 files uploaded', true);
      meta.textContent = remaining + ' of 3 still uploading';
    }
    function run(reset) {
      files.forEach(function (f) { set(f, reset ? 0 : +f.dataset.start); });
      title.textContent = 'Uploading 3 files'; meta.hidden = false; view.hidden = true; again.hidden = true; cancel.hidden = false;
      meta.textContent = 'About 20 seconds left';
      clearInterval(loop); loop = setInterval(tick, 600);
    }
    cancel.addEventListener('click', function () { finish('Uploads cancelled', false); again.focus(); });
    again.addEventListener('click', function () { run(true); cancel.focus(); });
    view.addEventListener('click', function (e) { e.preventDefault(); });
    run(false);
  }
})();

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-cal" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="5" width="17" height="15.5" rx="2.5"/><path d="M3.5 10h17M8 3v4M16 3v4"/></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-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s-7-6.2-7-11.5a7 7 0 0 1 14 0C19 14.8 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.5"/></symbol>
  <symbol id="i-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></symbol>
  <symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/></symbol>
  <symbol id="i-undo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></symbol>
</svg>

<div class="stack an" data-an-invites>
<div><span class="lbl">Invitations with Accept and Decline</span>
<ul class="list-group an-list">
  <li class="list-group-item d-flex gap-3" data-an-invite data-accepted="You joined Harbor Design System as an Editor." data-declined="Invitation declined. Dana won’t be notified.">
    <span class="an-av c1" aria-hidden="true">DW</span>
    <div class="an-body flex-grow-1"><b>Dana Whitfield</b> invited you to join the <b>Harbor Design System</b> workspace as an Editor.
      <span class="an-time">2 hours ago</span>
      <div class="d-flex flex-wrap gap-2 mt-3"><button class="btn btn-primary" type="button" data-an-accept>Accept</button><button class="btn btn-outline-secondary" type="button" data-an-decline>Decline</button></div>
      <p class="an-result" hidden tabindex="-1" data-an-result></p></div>
  </li>
  <li class="list-group-item d-flex gap-3" data-an-invite data-accepted="Added to your calendar for Thursday, 23 October." data-declined="You declined Design critique. Ravi will see your reply.">
    <span class="an-av cal" aria-hidden="true"><svg><use href="#i-cal"/></svg></span>
    <div class="an-body flex-grow-1"><b>Ravi Shah</b> invited you to <b>Design critique: checkout flow</b>
      <div class="an-detail d-flex flex-wrap column-gap-3 row-gap-1"><span class="d-inline-flex align-items-center gap-2"><svg aria-hidden="true"><use href="#i-clock"/></svg>Thu 23 Oct, 14:00–14:45</span><span class="d-inline-flex align-items-center gap-2"><svg aria-hidden="true"><use href="#i-pin"/></svg>Room 4B and video call</span></div>
      <div class="d-flex flex-wrap gap-2 mt-3"><button class="btn btn-primary" type="button" data-an-accept>Accept</button><button class="btn btn-outline-secondary" type="button" data-an-decline>Decline</button></div>
      <p class="an-result" hidden tabindex="-1" data-an-result></p></div>
  </li>
  <li class="list-group-item d-flex gap-3" data-an-invite data-accepted="Mateo can now view Q4 forecast.xlsx." data-declined="Request denied. Mateo will get an email.">
    <span class="an-av c2" aria-hidden="true">MR</span>
    <div class="an-body flex-grow-1"><b>Mateo Ruiz</b> requested view access to <b>Q4 forecast.xlsx</b>
      <span class="an-time">Yesterday at 16:20</span>
      <div class="d-flex flex-wrap gap-2 mt-3"><button class="btn btn-primary" type="button" data-an-accept>Approve</button><button class="btn btn-outline-secondary" type="button" data-an-decline>Deny</button></div>
      <p class="an-result" hidden tabindex="-1" data-an-result></p></div>
  </li>
</ul>
<p class="visually-hidden" role="status" data-an-status></p>
</div>
</div>

<div class="stack an">
<div><span class="lbl">Undo toast with a countdown</span>
<div class="d-flex flex-column align-items-start gap-3" data-an-undo-demo>
  <button class="btn btn-outline-secondary" type="button" data-an-delete>Delete conversation</button>
  <div class="toast an-snack show" data-an-snack>
    <div class="toast-body d-flex align-items-center gap-3">
      <p class="flex-grow-1 m-0"><b>Conversation deleted.</b> <span data-an-snack-text>“Q4 launch plan” with Ana Silva</span></p>
      <button class="btn an-undo" type="button" data-an-undo><svg width="15" height="15" aria-hidden="true"><use href="#i-undo"/></svg>Undo <span class="badge an-sec" data-an-sec>10s</span></button>
      <button class="btn-close btn-close-white" type="button" aria-label="Dismiss" data-an-snack-close></button>
    </div>
    <span class="an-bar" aria-hidden="true" data-an-bar></span>
  </div>
  <p class="visually-hidden" role="status" data-an-undo-status></p>
</div></div>

<div><span class="lbl">Upload progress toast</span>
<section class="toast an-up show" aria-labelledby="an-up-title" data-an-upload>
  <div class="toast-header gap-2"><h3 class="me-auto" id="an-up-title" data-an-up-title>Uploading 3 files</h3>
    <button class="btn-close" type="button" aria-label="Cancel uploads" data-an-cancel></button></div>
  <ul class="list-unstyled m-0 an-files">
    <li class="an-file" data-an-file data-rate="9" data-start="100">
      <span class="an-ficon" aria-hidden="true">PNG</span><span class="an-fname" id="an-f1">homepage-hero.png</span><span class="an-pct" data-an-pct>100%</span>
      <div class="progress" role="progressbar" aria-labelledby="an-f1" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"><div class="progress-bar" style="width:100%"></div></div>
    </li>
    <li class="an-file" data-an-file data-rate="6" data-start="62">
      <span class="an-ficon" aria-hidden="true">PDF</span><span class="an-fname" id="an-f2">brand-guidelines.pdf</span><span class="an-pct" data-an-pct>62%</span>
      <div class="progress" role="progressbar" aria-labelledby="an-f2" aria-valuemin="0" aria-valuemax="100" aria-valuenow="62"><div class="progress-bar" style="width:62%"></div></div>
    </li>
    <li class="an-file" data-an-file data-rate="3" data-start="18">
      <span class="an-ficon" aria-hidden="true">MP4</span><span class="an-fname" id="an-f3">launch-teaser.mp4</span><span class="an-pct" data-an-pct>18%</span>
      <div class="progress" role="progressbar" aria-labelledby="an-f3" aria-valuemin="0" aria-valuemax="100" aria-valuenow="18"><div class="progress-bar" style="width:18%"></div></div>
    </li>
  </ul>
  <div class="an-up-foot d-flex justify-content-between align-items-center gap-2"><span data-an-up-meta>About 20 seconds left</span><a class="an-link" href="#" hidden data-an-view>View files</a><button class="btn btn-outline-secondary" type="button" hidden data-an-again>Upload again</button></div>
  <p class="visually-hidden" role="status" data-an-up-status></p>
</section></div>
</div>
CSS
/* Actionable notifications on Bootstrap: .list-group invitations with .btn actions, a .toast undo snackbar with a visible
   countdown, and a .toast upload card with .progress bars. Results are announced through polite live regions. */
.an{--an-brand:#4f46e5;--an-ink:#0f172a;--an-text:#475569;--an-muted:#5b6474;--an-line:#e2e8f0;color:var(--an-text);font-size:.9rem;line-height:1.45}
.an .btn{--bs-btn-font-size:.8125rem;--bs-btn-font-weight:650;--bs-btn-border-radius:10px;--bs-btn-padding-x:14px;--bs-btn-focus-box-shadow:0 0 0 2px #fff,0 0 0 4px var(--an-brand);height:36px;display:inline-flex;align-items:center;gap:6px}
.an .btn-primary{--bs-btn-bg:var(--an-brand);--bs-btn-border-color:var(--an-brand);--bs-btn-hover-bg:#4338ca;--bs-btn-hover-border-color:#4338ca;--bs-btn-active-bg:#3730a3;--bs-btn-active-border-color:#3730a3}
.an .btn-outline-secondary{--bs-btn-color:var(--an-ink);--bs-btn-border-color:#cbd5e1;--bs-btn-hover-color:var(--an-ink);--bs-btn-hover-bg:#f1f5f9;--bs-btn-hover-border-color:#cbd5e1;
  --bs-btn-active-color:var(--an-ink);--bs-btn-active-bg:#e2e8f0;--bs-btn-active-border-color:#cbd5e1;background:#fff}
.an-list{--bs-list-group-border-color:var(--an-line);--bs-list-group-border-radius:18px;--bs-list-group-color:var(--an-text);--bs-list-group-item-padding-x:20px;--bs-list-group-item-padding-y:18px;
  box-shadow:0 1px 2px rgba(15,23,42,.05),0 20px 40px -30px rgba(15,23,42,.3)}
.an-av{flex:none;display:grid;place-items:center;width:42px;height:42px;border-radius:50%;font-size:.8125rem;font-weight:700}
.an-av svg{width:20px;height:20px}
.an-av.c1{background:#ddd6fe;color:#4c1d95}.an-av.c2{background:#fde68a;color:#78350f}.an-av.cal{border-radius:12px;background:#e0f2fe;color:#075985}
.an-body{min-width:0}
.an-body b{color:var(--an-ink);font-weight:650}
.an-detail{margin:8px 0 0;padding:10px 12px;border-radius:10px;background:#f8fafc;border:1px solid var(--an-line);font-size:.8125rem;color:#334155}
.an-detail svg{width:14px;height:14px;color:var(--an-muted)}
.an-time{display:block;margin-top:6px;font-size:.75rem;color:var(--an-muted)}
.an-result:not([hidden]){display:flex;align-items:center;gap:8px}
.an-result{margin:12px 0 0;padding:8px 12px;border-radius:10px;font-size:.8125rem;font-weight:600}
.an-result svg{flex:none;width:16px;height:16px}
.an-result[data-tone="ok"]{background:#f0fdf4;color:#166534}
.an-result[data-tone="no"]{background:#f1f5f9;color:#334155}
.an-result:focus-visible{outline:2px solid var(--an-brand);outline-offset:2px}
.an-snack{--bs-toast-max-width:420px;--bs-toast-bg:#0f172a;--bs-toast-color:#cbd5e1;--bs-toast-border-color:#0f172a;--bs-toast-border-radius:14px;--bs-toast-font-size:.9rem;
  --bs-toast-box-shadow:0 20px 40px -18px rgba(15,23,42,.55);position:relative;overflow:hidden}
.an-snack.show{animation:an-in .25s cubic-bezier(.2,.8,.2,1)}
.an-snack .toast-body{padding:12px 10px 12px 16px}
.an-snack b{color:#fff;font-weight:600}
.an-undo{--bs-btn-bg:rgba(165,180,252,.14);--bs-btn-color:#c7d2fe;--bs-btn-border-color:transparent;--bs-btn-hover-bg:rgba(165,180,252,.24);--bs-btn-hover-color:#c7d2fe;--bs-btn-hover-border-color:transparent;
  --bs-btn-active-bg:rgba(165,180,252,.3);--bs-btn-active-color:#c7d2fe;--bs-btn-padding-x:10px;--bs-btn-border-radius:9px;--bs-btn-font-weight:700;--bs-btn-focus-box-shadow:0 0 0 2px #a5b4fc;height:34px!important;flex:none}
.an-sec{--bs-badge-font-size:.8125rem;--bs-badge-font-weight:600;--bs-badge-padding-x:6px;--bs-badge-color:#e2e8f0;min-width:30px;background:rgba(255,255,255,.12);font-variant-numeric:tabular-nums}
.an-snack .btn-close{--bs-btn-close-focus-shadow:0 0 0 2px #a5b4fc;flex:none;width:30px;height:30px;padding:0;border-radius:8px;background-size:10px}
.an-bar{position:absolute;left:0;bottom:0;height:3px;width:100%;background:#818cf8;transform-origin:left;transform:scaleX(var(--an-left,1));transition:transform 1s linear}
.an-up{--bs-toast-max-width:420px;--bs-toast-border-color:var(--an-line);--bs-toast-border-radius:16px;--bs-toast-font-size:.9rem;--bs-toast-color:var(--an-text);--bs-toast-header-bg:#fff;--bs-toast-header-border-color:var(--an-line);
  --bs-toast-header-color:var(--an-ink);--bs-toast-padding-x:16px;--bs-toast-box-shadow:0 20px 40px -20px rgba(15,23,42,.38),0 2px 6px rgba(15,23,42,.06)}
.an-up .toast-header{padding:14px 12px 12px 16px}
.an-up .toast-header h3{margin:0;font-size:.9375rem;font-weight:700}
.an-up .btn-close{--bs-btn-close-focus-shadow:0 0 0 2px var(--an-brand);width:30px;height:30px;padding:0;margin:0;border-radius:8px;background-size:10px}
.an-files{padding:6px 16px 10px}
.an-file{display:grid;grid-template-columns:34px 1fr auto;align-items:center;gap:4px 12px;padding:10px 0}
.an-file+.an-file{border-top:1px dashed var(--an-line)}
.an-ficon{grid-row:span 2;display:grid;place-items:center;width:34px;height:34px;border-radius:9px;background:#eef2ff;color:var(--an-brand);font-size:.625rem;font-weight:800;letter-spacing:.03em}
.an-fname{min-width:0;overflow:hidden;color:var(--an-ink);font-weight:600;text-overflow:ellipsis;white-space:nowrap}
.an-pct{font-size:.75rem;font-weight:600;font-variant-numeric:tabular-nums;color:var(--an-muted)}
.an-file[data-done] .an-pct{color:#15803d}
.an-file .progress{--bs-progress-height:6px;--bs-progress-bg:#e2e8f0;--bs-progress-border-radius:6px;--bs-progress-bar-bg:var(--an-brand);--bs-progress-bar-transition:width .25s linear;grid-column:2 / -1}
.an-file[data-done] .progress{--bs-progress-bar-bg:#16a34a}
.an-file .progress-bar{border-radius:6px}
.an-up-foot{padding:10px 16px 14px;font-size:.8125rem;color:var(--an-muted)}
.an-link{color:#4338ca;font-weight:650;text-decoration:none}
.an-link:hover{color:#4338ca;text-decoration:underline}
.an-link:focus-visible{outline:2px solid var(--an-brand);outline-offset:2px}
@keyframes an-in{from{opacity:0;transform:translateY(10px)}}
@media (max-width:480px){.an-list{--bs-list-group-item-padding-x:16px;--bs-list-group-item-padding-y:16px}.an-av{width:36px;height:36px}}
@media (prefers-reduced-motion:reduce){.an-snack.show{animation:none}.an-bar{transition:none}.an-file .progress{--bs-progress-bar-transition:none}}
JavaScript
(function () {
  // 1. Invitations: answer in place, then move focus to the result so keyboard users are not dropped.
  document.querySelectorAll('[data-an-invite]').forEach(function (card) {
    var result = card.querySelector('[data-an-result]');
    var actions = card.querySelector('[data-an-accept]').parentNode;
    var status = card.closest('[data-an-invites]').querySelector('[data-an-status]');
    function answer(ok) {
      result.textContent = ok ? card.dataset.accepted : card.dataset.declined;
      result.dataset.tone = ok ? 'ok' : 'no';
      result.hidden = false; actions.hidden = true;
      result.focus();
      if (status) status.textContent = result.textContent;
    }
    card.querySelector('[data-an-accept]').addEventListener('click', function () { answer(true); });
    card.querySelector('[data-an-decline]').addEventListener('click', function () { answer(false); });
  });

  // 2. Undo toast: a one-second countdown that pauses while the pointer or keyboard focus is on the toast.
  var demo = document.querySelector('[data-an-undo-demo]');
  if (demo) {
    var snack = demo.querySelector('[data-an-snack]'), sec = demo.querySelector('[data-an-sec]'), bar = demo.querySelector('[data-an-bar]');
    var text = demo.querySelector('[data-an-snack-text]'), undo = demo.querySelector('[data-an-undo]');
    var say = demo.querySelector('[data-an-undo-status]');
    var TOTAL = 10, left = TOTAL, timer = null, paused = false;
    function render() { sec.textContent = left + 's'; bar.style.setProperty('--an-left', left / TOTAL); }
    function hide() { clearInterval(timer); snack.hidden = true; }
    function start() {
      left = TOTAL; render(); snack.hidden = false; undo.hidden = false;
      text.textContent = '“Q4 launch plan” with Ana Silva';
      snack.querySelector('b').textContent = 'Conversation deleted.';
      clearInterval(timer);
      say.textContent = 'Conversation deleted. Undo is available for ' + TOTAL + ' seconds.';
      timer = setInterval(function () { if (paused) return; left -= 1; render(); if (left <= 0) hide(); }, 1000);
    }
    ['mouseenter', 'focusin'].forEach(function (ev) { snack.addEventListener(ev, function () { paused = true; }); });
    snack.addEventListener('mouseleave', function () { paused = snack.contains(document.activeElement); });
    snack.addEventListener('focusout', function (e) { if (!snack.contains(e.relatedTarget)) paused = snack.matches(':hover'); });
    undo.addEventListener('click', function () {
      clearInterval(timer);
      snack.querySelector('b').textContent = 'Conversation restored.';
      text.textContent = 'It’s back in your inbox.';
      undo.hidden = true;
      say.textContent = 'Conversation restored.';
      demo.querySelector('[data-an-snack-close]').focus();
      timer = setTimeout(hide, 4000);
    });
    demo.querySelector('[data-an-snack-close]').addEventListener('click', function () { hide(); demo.querySelector('[data-an-delete]').focus(); });
    demo.querySelector('[data-an-delete]').addEventListener('click', start);
    start();
  }

  // 3. Upload toast: simulated progress. Replace tick() with your upload's progress events.
  var up = document.querySelector('[data-an-upload]');
  if (up) {
    var files = [].slice.call(up.querySelectorAll('[data-an-file]')), loop = null;
    var title = up.querySelector('[data-an-up-title]'), meta = up.querySelector('[data-an-up-meta]'), status2 = up.querySelector('[data-an-up-status]');
    var view = up.querySelector('[data-an-view]'), again = up.querySelector('[data-an-again]'), cancel = up.querySelector('[data-an-cancel]');
    function set(f, v) {
      v = Math.min(100, Math.round(v)); f.dataset.value = v;
      f.querySelector('[data-an-pct]').textContent = v + '%';
      f.querySelector('[role="progressbar"]').setAttribute('aria-valuenow', v);
      f.querySelector('[role="progressbar"] > div').style.width = v + '%';
      if (v >= 100) f.setAttribute('data-done', ''); else f.removeAttribute('data-done');
    }
    function finish(msg, done) {
      clearInterval(loop);
      title.textContent = msg; status2.textContent = msg;
      meta.hidden = true; view.hidden = !done; again.hidden = false; cancel.hidden = true;
    }
    function tick() {
      var remaining = 0;
      files.forEach(function (f) { var v = +f.dataset.value; if (v < 100) { set(f, v + +f.dataset.rate); } if (+f.dataset.value < 100) remaining++; });
      if (!remaining) return finish('3 files uploaded', true);
      meta.textContent = remaining + ' of 3 still uploading';
    }
    function run(reset) {
      files.forEach(function (f) { set(f, reset ? 0 : +f.dataset.start); });
      title.textContent = 'Uploading 3 files'; meta.hidden = false; view.hidden = true; again.hidden = true; cancel.hidden = false;
      meta.textContent = 'About 20 seconds left';
      clearInterval(loop); loop = setInterval(tick, 600);
    }
    cancel.addEventListener('click', function () { finish('Uploads cancelled', false); again.focus(); });
    again.addEventListener('click', function () { run(true); cancel.focus(); });
    view.addEventListener('click', function (e) { e.preventDefault(); });
    run(false);
  }
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-cal" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="5" width="17" height="15.5" rx="2.5"/><path d="M3.5 10h17M8 3v4M16 3v4"/></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-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s-7-6.2-7-11.5a7 7 0 0 1 14 0C19 14.8 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.5"/></symbol>
  <symbol id="i-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></symbol>
  <symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/></symbol>
  <symbol id="i-undo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></symbol>
</svg>

<div class="flex flex-col gap-11 text-[.9rem] leading-[1.45] text-[#475569]" data-an-invites>
<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Invitations with Accept and Decline</span>
<ul class="m-0 list-none rounded-[18px] border border-[#e2e8f0] bg-white p-0 shadow-[0_1px_2px_rgba(15,23,42,.05),0_20px_40px_-30px_rgba(15,23,42,.3)]">
  <li class="flex gap-3.5 border-t border-[#e2e8f0] px-5 py-[18px] first:border-t-0 max-[480px]:p-4" data-an-invite data-accepted="You joined Harbor Design System as an Editor." data-declined="Invitation declined. Dana won’t be notified.">
    <span class="grid size-[42px] flex-none place-items-center text-[.8125rem] font-bold max-[480px]:size-9 rounded-full bg-[#ddd6fe] text-[#4c1d95]" aria-hidden="true">DW</span>
    <div class="min-w-0 flex-1 [&_b]:font-[650] [&_b]:text-[#0f172a]"><b>Dana Whitfield</b> invited you to join the <b>Harbor Design System</b> workspace as an Editor.
      <span class="mt-1.5 block text-xs text-[#5b6474]">2 hours ago</span>
      <div class="mt-3 flex flex-wrap gap-2"><button class="inline-flex h-9 cursor-pointer items-center justify-center gap-1.5 rounded-[10px] border border-[#4f46e5] bg-[#4f46e5] px-3.5 text-[.8125rem] font-[650] text-white hover:bg-[#4338ca] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" data-an-accept>Accept</button><button class="inline-flex h-9 cursor-pointer items-center justify-center gap-1.5 rounded-[10px] border border-[#cbd5e1] bg-white px-3.5 text-[.8125rem] font-[650] text-[#0f172a] no-underline hover:bg-[#f1f5f9] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" data-an-decline>Decline</button></div>
      <p class="mt-3 mb-0 flex items-center gap-2 rounded-[10px] px-3 py-2 text-[.8125rem] font-semibold data-[tone=no]:bg-[#f1f5f9] data-[tone=no]:text-[#334155] data-[tone=ok]:bg-[#f0fdf4] data-[tone=ok]:text-[#166534] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" hidden tabindex="-1" data-an-result></p></div>
  </li>
  <li class="flex gap-3.5 border-t border-[#e2e8f0] px-5 py-[18px] first:border-t-0 max-[480px]:p-4" data-an-invite data-accepted="Added to your calendar for Thursday, 23 October." data-declined="You declined Design critique. Ravi will see your reply.">
    <span class="grid size-[42px] flex-none place-items-center text-[.8125rem] font-bold max-[480px]:size-9 rounded-xl bg-[#e0f2fe] text-[#075985]" aria-hidden="true"><svg class="size-5"><use href="#i-cal"/></svg></span>
    <div class="min-w-0 flex-1 [&_b]:font-[650] [&_b]:text-[#0f172a]"><b>Ravi Shah</b> invited you to <b>Design critique: checkout flow</b>
      <div class="mt-2 flex flex-wrap gap-x-3 gap-y-1.5 rounded-[10px] border border-[#e2e8f0] bg-[#f8fafc] px-3 py-2.5 text-[.8125rem] text-[#334155]"><span class="inline-flex items-center gap-1.5"><svg class="size-3.5 text-[#5b6474]" aria-hidden="true"><use href="#i-clock"/></svg>Thu 23 Oct, 14:00–14:45</span><span class="inline-flex items-center gap-1.5"><svg class="size-3.5 text-[#5b6474]" aria-hidden="true"><use href="#i-pin"/></svg>Room 4B and video call</span></div>
      <div class="mt-3 flex flex-wrap gap-2"><button class="inline-flex h-9 cursor-pointer items-center justify-center gap-1.5 rounded-[10px] border border-[#4f46e5] bg-[#4f46e5] px-3.5 text-[.8125rem] font-[650] text-white hover:bg-[#4338ca] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" data-an-accept>Accept</button><button class="inline-flex h-9 cursor-pointer items-center justify-center gap-1.5 rounded-[10px] border border-[#cbd5e1] bg-white px-3.5 text-[.8125rem] font-[650] text-[#0f172a] no-underline hover:bg-[#f1f5f9] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" data-an-decline>Decline</button></div>
      <p class="mt-3 mb-0 flex items-center gap-2 rounded-[10px] px-3 py-2 text-[.8125rem] font-semibold data-[tone=no]:bg-[#f1f5f9] data-[tone=no]:text-[#334155] data-[tone=ok]:bg-[#f0fdf4] data-[tone=ok]:text-[#166534] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" hidden tabindex="-1" data-an-result></p></div>
  </li>
  <li class="flex gap-3.5 border-t border-[#e2e8f0] px-5 py-[18px] first:border-t-0 max-[480px]:p-4" data-an-invite data-accepted="Mateo can now view Q4 forecast.xlsx." data-declined="Request denied. Mateo will get an email.">
    <span class="grid size-[42px] flex-none place-items-center text-[.8125rem] font-bold max-[480px]:size-9 rounded-full bg-[#fde68a] text-[#78350f]" aria-hidden="true">MR</span>
    <div class="min-w-0 flex-1 [&_b]:font-[650] [&_b]:text-[#0f172a]"><b>Mateo Ruiz</b> requested view access to <b>Q4 forecast.xlsx</b>
      <span class="mt-1.5 block text-xs text-[#5b6474]">Yesterday at 16:20</span>
      <div class="mt-3 flex flex-wrap gap-2"><button class="inline-flex h-9 cursor-pointer items-center justify-center gap-1.5 rounded-[10px] border border-[#4f46e5] bg-[#4f46e5] px-3.5 text-[.8125rem] font-[650] text-white hover:bg-[#4338ca] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" data-an-accept>Approve</button><button class="inline-flex h-9 cursor-pointer items-center justify-center gap-1.5 rounded-[10px] border border-[#cbd5e1] bg-white px-3.5 text-[.8125rem] font-[650] text-[#0f172a] no-underline hover:bg-[#f1f5f9] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" data-an-decline>Deny</button></div>
      <p class="mt-3 mb-0 flex items-center gap-2 rounded-[10px] px-3 py-2 text-[.8125rem] font-semibold data-[tone=no]:bg-[#f1f5f9] data-[tone=no]:text-[#334155] data-[tone=ok]:bg-[#f0fdf4] data-[tone=ok]:text-[#166534] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" hidden tabindex="-1" data-an-result></p></div>
  </li>
</ul>
<p class="sr-only" role="status" data-an-status></p>
</div>
</div>

<div class="flex flex-col gap-11 text-[.9rem] leading-[1.45] text-[#475569]">
<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Undo toast with a countdown</span>
<div class="flex flex-col items-start gap-3.5" data-an-undo-demo>
  <button class="inline-flex h-9 cursor-pointer items-center justify-center gap-1.5 rounded-[10px] border border-[#cbd5e1] bg-white px-3.5 text-[.8125rem] font-[650] text-[#0f172a] no-underline hover:bg-[#f1f5f9] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" data-an-delete>Delete conversation</button>
  <div class="relative flex w-full max-w-[420px] animate-snack-in items-center gap-3 overflow-hidden rounded-[14px] bg-[#0f172a] py-3 pr-2.5 pl-4 text-[#cbd5e1] shadow-[0_20px_40px_-18px_rgba(15,23,42,.55)] motion-reduce:animate-none" data-an-snack>
    <p class="m-0 min-w-0 flex-1"><b class="font-semibold text-white">Conversation deleted.</b> <span data-an-snack-text>“Q4 launch plan” with Ana Silva</span></p>
    <button class="inline-flex h-[34px] flex-none cursor-pointer items-center gap-2 rounded-[9px] border-0 bg-[#a5b4fc]/14 pr-2.5 pl-3 text-[.8125rem] font-bold text-[#c7d2fe] hover:bg-[#a5b4fc]/24 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#a5b4fc]" type="button" data-an-undo><svg width="15" height="15" aria-hidden="true"><use href="#i-undo"/></svg>Undo <span class="min-w-[30px] rounded-md bg-white/12 px-1.5 py-px text-center font-semibold text-[#e2e8f0] tabular-nums" data-an-sec>10s</span></button>
    <button class="grid size-[30px] flex-none cursor-pointer place-items-center rounded-lg border-0 bg-transparent p-0 text-[#cbd5e1] hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#a5b4fc]" type="button" aria-label="Dismiss" data-an-snack-close><svg class="size-3.5" aria-hidden="true"><use href="#i-x"/></svg></button>
    <span class="absolute bottom-0 left-0 h-[3px] w-full origin-left scale-x-(--an-left,1) bg-[#818cf8] transition-transform duration-1000 ease-linear motion-reduce:transition-none" aria-hidden="true" data-an-bar></span>
  </div>
  <p class="sr-only" role="status" data-an-undo-status></p>
</div></div>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Upload progress toast</span>
<section class="w-full max-w-[420px] rounded-2xl border border-[#e2e8f0] bg-white shadow-[0_20px_40px_-20px_rgba(15,23,42,.38),0_2px_6px_rgba(15,23,42,.06)]" aria-labelledby="an-up-title" data-an-upload>
  <div class="flex items-center gap-2.5 border-b border-[#e2e8f0] pt-3.5 pr-3 pb-3 pl-4"><h3 class="m-0 flex-1 text-[.9375rem] font-bold text-[#0f172a]" id="an-up-title" data-an-up-title>Uploading 3 files</h3>
    <button class="grid size-[30px] flex-none cursor-pointer place-items-center rounded-lg border-0 bg-transparent p-0 text-[#64748b] hover:bg-[#f1f5f9] hover:text-[#0f172a] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" aria-label="Cancel uploads" data-an-cancel><svg class="size-3.5" aria-hidden="true"><use href="#i-x"/></svg></button></div>
  <ul class="m-0 list-none px-4 pt-1.5 pb-2.5">
    <li class="group grid grid-cols-[34px_1fr_auto] items-center gap-x-3 gap-y-1 border-t border-dashed border-[#e2e8f0] py-2.5 first:border-t-0" data-an-file data-rate="9" data-start="100">
      <span class="row-span-2 grid size-[34px] place-items-center rounded-[9px] bg-[#eef2ff] text-[.625rem] font-extrabold tracking-[.03em] text-[#4f46e5]" aria-hidden="true">PNG</span><span class="min-w-0 truncate font-semibold text-[#0f172a]" id="an-f1">homepage-hero.png</span><span class="text-xs font-semibold text-[#5b6474] tabular-nums group-data-[done]:text-[#15803d]" data-an-pct>100%</span>
      <div class="col-[2/-1] h-1.5 overflow-hidden rounded-md bg-[#e2e8f0]" role="progressbar" aria-labelledby="an-f1" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"><div class="h-full rounded-[inherit] bg-[#4f46e5] transition-[width] duration-250 ease-linear group-data-[done]:bg-[#16a34a] motion-reduce:transition-none" style="width:100%"></div></div>
    </li>
    <li class="group grid grid-cols-[34px_1fr_auto] items-center gap-x-3 gap-y-1 border-t border-dashed border-[#e2e8f0] py-2.5 first:border-t-0" data-an-file data-rate="6" data-start="62">
      <span class="row-span-2 grid size-[34px] place-items-center rounded-[9px] bg-[#eef2ff] text-[.625rem] font-extrabold tracking-[.03em] text-[#4f46e5]" aria-hidden="true">PDF</span><span class="min-w-0 truncate font-semibold text-[#0f172a]" id="an-f2">brand-guidelines.pdf</span><span class="text-xs font-semibold text-[#5b6474] tabular-nums group-data-[done]:text-[#15803d]" data-an-pct>62%</span>
      <div class="col-[2/-1] h-1.5 overflow-hidden rounded-md bg-[#e2e8f0]" role="progressbar" aria-labelledby="an-f2" aria-valuemin="0" aria-valuemax="100" aria-valuenow="62"><div class="h-full rounded-[inherit] bg-[#4f46e5] transition-[width] duration-250 ease-linear group-data-[done]:bg-[#16a34a] motion-reduce:transition-none" style="width:62%"></div></div>
    </li>
    <li class="group grid grid-cols-[34px_1fr_auto] items-center gap-x-3 gap-y-1 border-t border-dashed border-[#e2e8f0] py-2.5 first:border-t-0" data-an-file data-rate="3" data-start="18">
      <span class="row-span-2 grid size-[34px] place-items-center rounded-[9px] bg-[#eef2ff] text-[.625rem] font-extrabold tracking-[.03em] text-[#4f46e5]" aria-hidden="true">MP4</span><span class="min-w-0 truncate font-semibold text-[#0f172a]" id="an-f3">launch-teaser.mp4</span><span class="text-xs font-semibold text-[#5b6474] tabular-nums group-data-[done]:text-[#15803d]" data-an-pct>18%</span>
      <div class="col-[2/-1] h-1.5 overflow-hidden rounded-md bg-[#e2e8f0]" role="progressbar" aria-labelledby="an-f3" aria-valuemin="0" aria-valuemax="100" aria-valuenow="18"><div class="h-full rounded-[inherit] bg-[#4f46e5] transition-[width] duration-250 ease-linear group-data-[done]:bg-[#16a34a] motion-reduce:transition-none" style="width:18%"></div></div>
    </li>
  </ul>
  <div class="flex items-center justify-between gap-2.5 px-4 pt-2.5 pb-3.5 text-[.8125rem] text-[#5b6474]"><span data-an-up-meta>About 20 seconds left</span><a class="font-[650] text-[#4338ca] no-underline hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" href="#" hidden data-an-view>View files</a><button class="inline-flex h-9 cursor-pointer items-center justify-center gap-1.5 rounded-[10px] border border-[#cbd5e1] bg-white px-3.5 text-[.8125rem] font-[650] text-[#0f172a] no-underline hover:bg-[#f1f5f9] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" hidden data-an-again>Upload again</button></div>
  <p class="sr-only" role="status" data-an-up-status></p>
</section></div>
</div>
CSS
@theme {
  --animate-snack-in: snack-in .25s cubic-bezier(.2,.8,.2,1);
  @keyframes snack-in { from { opacity: 0; transform: translateY(10px); } }
}
JavaScript
(function () {
  // 1. Invitations: answer in place, then move focus to the result so keyboard users are not dropped.
  document.querySelectorAll('[data-an-invite]').forEach(function (card) {
    var result = card.querySelector('[data-an-result]');
    var actions = card.querySelector('[data-an-accept]').parentNode;
    var status = card.closest('[data-an-invites]').querySelector('[data-an-status]');
    function answer(ok) {
      result.textContent = ok ? card.dataset.accepted : card.dataset.declined;
      result.dataset.tone = ok ? 'ok' : 'no';
      result.hidden = false; actions.hidden = true;
      result.focus();
      if (status) status.textContent = result.textContent;
    }
    card.querySelector('[data-an-accept]').addEventListener('click', function () { answer(true); });
    card.querySelector('[data-an-decline]').addEventListener('click', function () { answer(false); });
  });

  // 2. Undo toast: a one-second countdown that pauses while the pointer or keyboard focus is on the toast.
  var demo = document.querySelector('[data-an-undo-demo]');
  if (demo) {
    var snack = demo.querySelector('[data-an-snack]'), sec = demo.querySelector('[data-an-sec]'), bar = demo.querySelector('[data-an-bar]');
    var text = demo.querySelector('[data-an-snack-text]'), undo = demo.querySelector('[data-an-undo]');
    var say = demo.querySelector('[data-an-undo-status]');
    var TOTAL = 10, left = TOTAL, timer = null, paused = false;
    function render() { sec.textContent = left + 's'; bar.style.setProperty('--an-left', left / TOTAL); }
    function hide() { clearInterval(timer); snack.hidden = true; }
    function start() {
      left = TOTAL; render(); snack.hidden = false; undo.hidden = false;
      text.textContent = '“Q4 launch plan” with Ana Silva';
      snack.querySelector('b').textContent = 'Conversation deleted.';
      clearInterval(timer);
      say.textContent = 'Conversation deleted. Undo is available for ' + TOTAL + ' seconds.';
      timer = setInterval(function () { if (paused) return; left -= 1; render(); if (left <= 0) hide(); }, 1000);
    }
    ['mouseenter', 'focusin'].forEach(function (ev) { snack.addEventListener(ev, function () { paused = true; }); });
    snack.addEventListener('mouseleave', function () { paused = snack.contains(document.activeElement); });
    snack.addEventListener('focusout', function (e) { if (!snack.contains(e.relatedTarget)) paused = snack.matches(':hover'); });
    undo.addEventListener('click', function () {
      clearInterval(timer);
      snack.querySelector('b').textContent = 'Conversation restored.';
      text.textContent = 'It’s back in your inbox.';
      undo.hidden = true;
      say.textContent = 'Conversation restored.';
      demo.querySelector('[data-an-snack-close]').focus();
      timer = setTimeout(hide, 4000);
    });
    demo.querySelector('[data-an-snack-close]').addEventListener('click', function () { hide(); demo.querySelector('[data-an-delete]').focus(); });
    demo.querySelector('[data-an-delete]').addEventListener('click', start);
    start();
  }

  // 3. Upload toast: simulated progress. Replace tick() with your upload's progress events.
  var up = document.querySelector('[data-an-upload]');
  if (up) {
    var files = [].slice.call(up.querySelectorAll('[data-an-file]')), loop = null;
    var title = up.querySelector('[data-an-up-title]'), meta = up.querySelector('[data-an-up-meta]'), status2 = up.querySelector('[data-an-up-status]');
    var view = up.querySelector('[data-an-view]'), again = up.querySelector('[data-an-again]'), cancel = up.querySelector('[data-an-cancel]');
    function set(f, v) {
      v = Math.min(100, Math.round(v)); f.dataset.value = v;
      f.querySelector('[data-an-pct]').textContent = v + '%';
      f.querySelector('[role="progressbar"]').setAttribute('aria-valuenow', v);
      f.querySelector('[role="progressbar"] > div').style.width = v + '%';
      if (v >= 100) f.setAttribute('data-done', ''); else f.removeAttribute('data-done');
    }
    function finish(msg, done) {
      clearInterval(loop);
      title.textContent = msg; status2.textContent = msg;
      meta.hidden = true; view.hidden = !done; again.hidden = false; cancel.hidden = true;
    }
    function tick() {
      var remaining = 0;
      files.forEach(function (f) { var v = +f.dataset.value; if (v < 100) { set(f, v + +f.dataset.rate); } if (+f.dataset.value < 100) remaining++; });
      if (!remaining) return finish('3 files uploaded', true);
      meta.textContent = remaining + ' of 3 still uploading';
    }
    function run(reset) {
      files.forEach(function (f) { set(f, reset ? 0 : +f.dataset.start); });
      title.textContent = 'Uploading 3 files'; meta.hidden = false; view.hidden = true; again.hidden = true; cancel.hidden = false;
      meta.textContent = 'About 20 seconds left';
      clearInterval(loop); loop = setInterval(tick, 600);
    }
    cancel.addEventListener('click', function () { finish('Uploads cancelled', false); again.focus(); });
    again.addEventListener('click', function () { run(true); cancel.focus(); });
    view.addEventListener('click', function (e) { e.preventDefault(); });
    run(false);
  }
})();

About this set

Notifications people act on without leaving the page. The invitations list holds a workspace invite with Accept and Decline, a calendar invite that shows the time and room, and an access request with Approve and Deny. Answering replaces the buttons with a result line, such as You joined Harbor Design System as an Editor, which takes focus and is announced. The undo toast is a dark snackbar for a deleted conversation with an Undo button that counts down from ten seconds and a bar that shrinks with it; hovering or focusing the toast pauses the countdown, Undo restores the conversation and says so, and a Delete conversation button brings the toast back. The upload toast lists three files with a type tile, a percentage and a progress bar each, says how many are still uploading, and ends with 3 files uploaded, a View files link and Upload again; the close button cancels. The progress is simulated by a small timer, so swap tick() for your upload’s progress events. Use the invitations in a notification centre and the two toasts in a corner of the app.

What’s included

  • Accept and Decline answered in place, with focus moved to the result
  • Undo snackbar with a visible ten-second countdown that pauses on hover and focus
  • Upload toast with one labelled progress bar per file
  • Live region messages for every result, announced once, not every second
  • Bootstrap .list-group, .toast, .progress and .badge without its JavaScript

Accessibility

Each result line is focusable with tabindex="-1" and receives focus after Accept or Decline, and polite status regions announce invitation results, the delete and restore, and the upload outcome; the countdown itself is not announced every second. Progress bars use role="progressbar" with aria-valuenow and are labelled by their file name. Every button has a visible focus outline, and the toast entrance and bar transitions are off under prefers-reduced-motion.

Customise it

In plain CSS change --an-brand, --an-ink, --an-text and --an-line on .an, the snackbar colours on .an-snack and the countdown length in the TOTAL constant. In Bootstrap restyle through --bs-btn-*, --bs-toast-* and --bs-progress-* variables; in Tailwind edit the hex values in the bg-, border- and text- utilities.