Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 12.5 4 4 8-9"/></symbol>
  <symbol id="i-error" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v6M12 17h.01"/></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.6 17.5A2 2 0 0 0 4.3 20.5h15.4a2 2 0 0 0 1.7-3L13.7 4.2a2 2 0 0 0-3.4 0z"/><path d="M12 9.5v4M12 17h.01"/></symbol>
  <symbol id="i-info" 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 11v5M12 8h.01"/></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>
</svg>

<div><span class="lbl">Live stack: auto-dismiss with a visible timer, paused on hover or focus</span>
<div class="stage">
  <div class="stage-bar"><b>Northwind · Projects</b>
    <span class="nt-pos"><label class="nt-pos-lbl" for="nt-pos">Position</label>
    <select class="nt-select" id="nt-pos" data-toast-position>
      <option value="tr">Top right</option><option value="tc">Top centre</option><option value="br">Bottom right</option><option value="bl">Bottom left</option>
    </select></span>
    <button class="nt-btn" type="button" data-toast-show="mention">Get a mention</button>
    <button class="nt-btn" type="button" data-toast-show="success">Send invoice</button>
    <button class="nt-btn" type="button" data-toast-show="error">Sync Drive</button>
  </div>
  <div class="stage-body">
  <div class="stage-rows" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i></div>

  <div class="nt-stack" data-pos="tr" role="status" aria-live="polite" aria-atomic="false" aria-label="Notifications" data-toast-stack>
    <div class="nt-toast" data-toast style="--nt-time:9s">
      <span class="nt-avatar" aria-hidden="true">PR</span>
      <div class="nt-body"><p class="nt-title">Priya Raman mentioned you</p><p class="nt-text">“@Sam can you check the Q4 pricing copy before Friday?”</p><p class="nt-meta">Atlas redesign · just now</p></div>
      <button class="nt-close" type="button" data-toast-close aria-label="Dismiss notification"><svg class="nt-ring" viewBox="0 0 36 36" aria-hidden="true"><circle class="nt-track" cx="18" cy="18" r="16"/><circle class="nt-time" cx="18" cy="18" r="16" pathLength="100"/></svg><svg class="nt-x" aria-hidden="true"><use href="#i-x"/></svg></button>
    </div>
    <div class="nt-toast nt-success" data-toast style="--nt-time:7s">
      <span class="nt-icon" aria-hidden="true"><svg><use href="#i-ok"/></svg></span>
      <div class="nt-body"><p class="nt-title">Invoice INV-2041 sent</p><p class="nt-text">Bramble & Co. will get it at [email protected].</p></div>
      <button class="nt-close" type="button" data-toast-close aria-label="Dismiss notification"><svg class="nt-ring" viewBox="0 0 36 36" aria-hidden="true"><circle class="nt-track" cx="18" cy="18" r="16"/><circle class="nt-time" cx="18" cy="18" r="16" pathLength="100"/></svg><svg class="nt-x" aria-hidden="true"><use href="#i-x"/></svg></button>
    </div>
    <div class="nt-toast nt-error" role="alert" data-toast>
      <span class="nt-icon" aria-hidden="true"><svg><use href="#i-error"/></svg></span>
      <div class="nt-body"><p class="nt-title">Google Drive sync failed</p><p class="nt-text">We couldn’t reach Drive. Your 3 new files are saved locally.</p>
        <div class="nt-actions"><button class="nt-act nt-act-primary" type="button" data-toast-retry>Retry sync</button></div></div>
      <button class="nt-close" type="button" data-toast-close aria-label="Dismiss notification"><svg class="nt-x" aria-hidden="true"><use href="#i-x"/></svg></button>
    </div>
  </div>

  </div>

  <!-- Templates the script clones. Errors have no timer: they stay until dismissed. -->
  <template data-toast-template="mention"><div class="nt-toast" data-toast style="--nt-time:9s"><span class="nt-avatar" aria-hidden="true" data-avatar></span><div class="nt-body"><p class="nt-title" data-title></p><p class="nt-text" data-text></p><p class="nt-meta" data-meta></p></div><button class="nt-close" type="button" data-toast-close aria-label="Dismiss notification"><svg class="nt-ring" viewBox="0 0 36 36" aria-hidden="true"><circle class="nt-track" cx="18" cy="18" r="16"/><circle class="nt-time" cx="18" cy="18" r="16" pathLength="100"/></svg><svg class="nt-x" aria-hidden="true"><use href="#i-x"/></svg></button></div></template>
  <template data-toast-template="success"><div class="nt-toast nt-success" data-toast style="--nt-time:6s"><span class="nt-icon" aria-hidden="true"><svg><use href="#i-ok"/></svg></span><div class="nt-body"><p class="nt-title" data-title></p><p class="nt-text" data-text></p></div><button class="nt-close" type="button" data-toast-close aria-label="Dismiss notification"><svg class="nt-ring" viewBox="0 0 36 36" aria-hidden="true"><circle class="nt-track" cx="18" cy="18" r="16"/><circle class="nt-time" cx="18" cy="18" r="16" pathLength="100"/></svg><svg class="nt-x" aria-hidden="true"><use href="#i-x"/></svg></button></div></template>
  <template data-toast-template="error"><div class="nt-toast nt-error" role="alert" data-toast><span class="nt-icon" aria-hidden="true"><svg><use href="#i-error"/></svg></span><div class="nt-body"><p class="nt-title" data-title></p><p class="nt-text" data-text></p><div class="nt-actions"><button class="nt-act nt-act-primary" type="button" data-toast-retry>Retry sync</button></div></div><button class="nt-close" type="button" data-toast-close aria-label="Dismiss notification"><svg class="nt-x" aria-hidden="true"><use href="#i-x"/></svg></button></div></template>
</div></div>

<div><span class="lbl">Static variants: info, success, warning and error</span>
<div class="grid2">
  <div class="nt-toast nt-static" data-toast>
    <span class="nt-icon" aria-hidden="true"><svg><use href="#i-info"/></svg></span>
    <div class="nt-body"><p class="nt-title">Scheduled maintenance</p><p class="nt-text">Reports are read-only on Sunday from 02:00 to 03:00 UTC.</p></div>
    <button class="nt-close" type="button" data-toast-close aria-label="Dismiss notification"><svg class="nt-x" aria-hidden="true"><use href="#i-x"/></svg></button>
  </div>
  <div class="nt-toast nt-success nt-static" data-toast>
    <span class="nt-icon" aria-hidden="true"><svg><use href="#i-ok"/></svg></span>
    <div class="nt-body"><p class="nt-title">Payment received</p><p class="nt-text">$1,240.00 from Harbor Labs was added to your balance.</p></div>
    <button class="nt-close" type="button" data-toast-close aria-label="Dismiss notification"><svg class="nt-x" aria-hidden="true"><use href="#i-x"/></svg></button>
  </div>
  <div class="nt-toast nt-warning nt-static" data-toast>
    <span class="nt-icon" aria-hidden="true"><svg><use href="#i-warn"/></svg></span>
    <div class="nt-body"><p class="nt-title">Storage almost full</p><p class="nt-text">You’ve used 9.4 of 10 GB. Old exports are removed after 30 days.</p></div>
    <button class="nt-close" type="button" data-toast-close aria-label="Dismiss notification"><svg class="nt-x" aria-hidden="true"><use href="#i-x"/></svg></button>
  </div>
  <div class="nt-toast nt-error nt-static" data-toast>
    <span class="nt-icon" aria-hidden="true"><svg><use href="#i-error"/></svg></span>
    <div class="nt-body"><p class="nt-title">Card declined</p><p class="nt-text">The Visa ending 4418 was declined. Update it to keep your plan.</p></div>
    <button class="nt-close" type="button" data-toast-close aria-label="Dismiss notification"><svg class="nt-x" aria-hidden="true"><use href="#i-x"/></svg></button>
  </div>
</div></div>
CSS
/* Notification toast stack. The ring around the close button is the timer: its CSS animation runs for --nt-time
   and the script dismisses the toast on animationend, so pausing the animation (hover, keyboard focus) pauses the countdown. */
.nt-stack,.nt-toast{--nt-brand:#4f46e5;--nt-ink:#0f172a;--nt-text:#475569;--nt-muted:#5b6474;--nt-line:#e2e8f0}
.nt-stack{position:fixed;z-index:1090;display:flex;flex-direction:column-reverse;gap:8px;width:min(380px,calc(100% - 32px));margin:0;padding:0;
  top:16px;right:16px}
.nt-stack[data-pos="tc"]{right:auto;left:50%;transform:translateX(-50%)}
.nt-stack[data-pos="br"]{top:auto;bottom:16px;flex-direction:column}
.nt-stack[data-pos="bl"]{top:auto;right:auto;bottom:16px;left:16px;flex-direction:column}
.nt-toast{--nt-accent:var(--nt-brand);--nt-soft:#eef2ff;--nt-from:-12px;position:relative;display:flex;align-items:flex-start;gap:12px;padding:14px 12px 14px 14px;
  background:#fff;color:var(--nt-text);border:1px solid var(--nt-line);border-radius:16px;font-size:.875rem;line-height:1.45;
  box-shadow:0 20px 40px -20px rgba(15,23,42,.38),0 2px 6px rgba(15,23,42,.06);animation:nt-in .3s cubic-bezier(.2,.8,.2,1)}
.nt-stack[data-pos^="b"] .nt-toast{--nt-from:12px}
.nt-toast.is-leaving{animation:nt-out .2s ease-in forwards}
.nt-success{--nt-accent:#15803d;--nt-soft:#dcfce7}
.nt-warning{--nt-accent:#b45309;--nt-soft:#fef3c7}
.nt-error{--nt-accent:#dc2626;--nt-soft:#fee2e2;border-color:#fecaca}
.nt-icon,.nt-avatar{flex:none;display:grid;place-items:center;width:36px;height:36px;border-radius:50%;background:var(--nt-soft);color:var(--nt-accent)}
.nt-icon svg{width:19px;height:19px}
.nt-avatar{background:#ddd6fe;color:#4c1d95;font-size:.8125rem;font-weight:700;letter-spacing:.02em}
.nt-body{flex:1;min-width:0}
.nt-title{margin:0;font-size:.9375rem;font-weight:600;color:var(--nt-ink)}
.nt-text{margin:2px 0 0}
.nt-meta{margin:6px 0 0;font-size:.75rem;color:var(--nt-muted)}
.nt-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
.nt-act{height:32px;padding:0 12px;border:1px solid #cbd5e1;border-radius:9px;background:#fff;color:var(--nt-ink);font:inherit;font-size:.8125rem;font-weight:600;cursor:pointer}
.nt-act:hover{background:#f1f5f9}
.nt-act-primary{border-color:var(--nt-accent);background:var(--nt-accent);color:#fff}
.nt-act-primary:hover{background:var(--nt-accent);filter:brightness(.92)}
/* Close button with the timer ring around it */
.nt-close{position:relative;flex:none;display:grid;place-items:center;width:34px;height:34px;margin:-4px -2px 0 0;padding:0;border:0;border-radius:50%;
  background:transparent;color:#64748b;cursor:pointer}
.nt-close:hover{background:#f1f5f9;color:var(--nt-ink)}
.nt-close .nt-x{width:14px;height:14px}
.nt-ring{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg);pointer-events:none}
.nt-ring circle{fill:none;stroke-width:2.5}
.nt-ring .nt-track{stroke:#e2e8f0}
.nt-ring .nt-time{stroke:var(--nt-accent);stroke-dasharray:100;stroke-dashoffset:0;stroke-linecap:round;animation:nt-time var(--nt-time,6s) linear forwards}
.nt-stack:hover .nt-time,.nt-stack:focus-within .nt-time{animation-play-state:paused}
.nt-act:focus-visible,.nt-close:focus-visible{outline:2px solid var(--nt-brand);outline-offset:2px}
@keyframes nt-in{from{opacity:0;transform:translateY(var(--nt-from)) scale(.97)}}
@keyframes nt-out{to{opacity:0;transform:scale(.96)}}
@keyframes nt-time{to{stroke-dashoffset:100}}
/* Static variants (no timer) for in-page use */
.nt-static{position:static;animation:none;box-shadow:0 1px 2px rgba(15,23,42,.05),0 12px 28px -20px rgba(15,23,42,.35)}
@media (max-width:480px){.nt-stack{width:calc(100% - 24px)}.nt-stack,.nt-stack[data-pos]{left:12px;right:12px;transform:none}}
@media (prefers-reduced-motion:reduce){.nt-toast,.nt-toast.is-leaving{animation:none}}
JavaScript
(function () {
  var stack = document.querySelector('[data-toast-stack]');
  if (!stack) return;
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
  var MAX = 4, lastTrigger = null, n = 0;
  var content = {
    mention: [
      { avatar: 'LO', title: 'Leo Okafor replied to you', text: '“Pushed the new hero copy, take a look when you can.”', meta: 'Website launch · just now' },
      { avatar: 'MK', title: 'Mina Kovač mentioned you', text: '“@Sam the client approved option B.”', meta: 'Bramble rebrand · just now' }
    ],
    success: [{ title: 'Invoice INV-2042 sent', text: 'Harbor Labs will get it at [email protected].' }],
    error: [{ title: 'Google Drive sync failed', text: 'We couldn’t reach Drive. Your files are saved locally.' }]
  };

  function arm(toast) {
    var timer = toast.querySelector('.nt-time');
    if (timer) timer.addEventListener('animationend', function () { dismiss(toast); });
  }

  function dismiss(toast) {
    if (!toast.isConnected || toast.classList.contains('is-leaving')) return;
    var hadFocus = toast.contains(document.activeElement);
    var sibling = toast.nextElementSibling || toast.previousElementSibling;
    toast.classList.add('is-leaving');
    function done() {
      toast.remove();
      if (!hadFocus) return;
      var target = (sibling && sibling.matches('[data-toast]') && sibling.querySelector('button')) || lastTrigger;
      if (target) target.focus();
    }
    if (reduce.matches || getComputedStyle(toast).animationName === 'none') return done();
    toast.addEventListener('animationend', function (e) { if (e.target === toast) done(); });
  }

  function show(type, c) {
    var tpl = document.querySelector('[data-toast-template="' + type + '"]');
    if (!tpl) return;
    var list = content[type];
    c = c || list[n++ % list.length];
    var toast = tpl.content.firstElementChild.cloneNode(true);
    ['avatar', 'title', 'text', 'meta'].forEach(function (k) {
      var el = toast.querySelector('[data-' + k + ']');
      if (el) el.textContent = c[k] || '';
    });
    stack.appendChild(toast);
    arm(toast);
    var live = stack.querySelectorAll('[data-toast]:not(.is-leaving)');
    if (live.length > MAX) dismiss(live[0]);
  }

  stack.querySelectorAll('[data-toast]').forEach(arm);

  document.addEventListener('click', function (e) {
    var btn = e.target.closest('[data-toast-show]');
    if (btn) { lastTrigger = btn; show(btn.dataset.toastShow); return; }
    var toast = e.target.closest('[data-toast]');
    if (!toast) return;
    if (e.target.closest('[data-toast-close]')) dismiss(toast);
    if (e.target.closest('[data-toast-retry]')) { dismiss(toast); show('success', { title: 'Drive is back in sync', text: '3 files uploaded to Northwind / Projects.' }); }
  });

  // Esc dismisses the toast that has focus.
  document.addEventListener('keydown', function (e) {
    var toast = e.key === 'Escape' && e.target.closest && e.target.closest('[data-toast]');
    if (toast) dismiss(toast);
  });

  var pos = document.querySelector('[data-toast-position]');
  if (pos) pos.addEventListener('change', function () { stack.dataset.pos = pos.value; });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 12.5 4 4 8-9"/></symbol>
  <symbol id="i-error" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v6M12 17h.01"/></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.6 17.5A2 2 0 0 0 4.3 20.5h15.4a2 2 0 0 0 1.7-3L13.7 4.2a2 2 0 0 0-3.4 0z"/><path d="M12 9.5v4M12 17h.01"/></symbol>
  <symbol id="i-info" 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 11v5M12 8h.01"/></symbol>
</svg>

<div><span class="lbl">Live stack: auto-dismiss with a visible timer, paused on hover or focus</span>
<div class="stage">
  <div class="stage-bar"><b>Northwind · Projects</b>
    <span class="d-inline-flex align-items-center gap-2"><label class="nt-pos-lbl" for="nt-pos">Position</label>
    <select class="form-select nt-select" id="nt-pos" data-toast-position>
      <option value="tr">Top right</option><option value="tc">Top centre</option><option value="br">Bottom right</option><option value="bl">Bottom left</option>
    </select></span>
    <button class="btn nt-btn" type="button" data-toast-show="mention">Get a mention</button>
    <button class="btn nt-btn" type="button" data-toast-show="success">Send invoice</button>
    <button class="btn nt-btn" type="button" data-toast-show="error">Sync Drive</button>
  </div>
  <div class="stage-body">
  <div class="stage-rows" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i></div>

  <div class="toast-container position-fixed top-0 end-0 p-3 d-flex flex-column-reverse gap-2 nt-stack" role="status" aria-live="polite" aria-atomic="false" aria-label="Notifications" data-toast-stack>
    <div class="toast nt-toast fade show" data-bs-autohide="false" data-toast style="--nt-time:9s">
      <div class="toast-body"><span class="nt-avatar" aria-hidden="true">PR</span>
        <div class="flex-grow-1 nt-body"><p class="nt-title">Priya Raman mentioned you</p><p class="nt-text">“@Sam can you check the Q4 pricing copy before Friday?”</p><p class="nt-meta">Atlas redesign · just now</p></div>
        <span class="nt-close"><svg class="nt-ring" viewBox="0 0 36 36" aria-hidden="true"><circle class="nt-track" cx="18" cy="18" r="16"/><circle class="nt-time" cx="18" cy="18" r="16" pathLength="100"/></svg><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Dismiss notification"></button></span></div>
    </div>
    <div class="toast nt-toast nt-success fade show" data-bs-autohide="false" data-toast style="--nt-time:7s">
      <div class="toast-body"><span class="nt-icon" aria-hidden="true"><svg><use href="#i-ok"/></svg></span>
        <div class="flex-grow-1 nt-body"><p class="nt-title">Invoice INV-2041 sent</p><p class="nt-text">Bramble & Co. will get it at [email protected].</p></div>
        <span class="nt-close"><svg class="nt-ring" viewBox="0 0 36 36" aria-hidden="true"><circle class="nt-track" cx="18" cy="18" r="16"/><circle class="nt-time" cx="18" cy="18" r="16" pathLength="100"/></svg><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Dismiss notification"></button></span></div>
    </div>
    <div class="toast nt-toast nt-error fade show" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="false" data-toast>
      <div class="toast-body"><span class="nt-icon" aria-hidden="true"><svg><use href="#i-error"/></svg></span>
        <div class="flex-grow-1 nt-body"><p class="nt-title">Google Drive sync failed</p><p class="nt-text">We couldn’t reach Drive. Your 3 new files are saved locally.</p>
          <div class="d-flex gap-2 mt-2"><button class="btn btn-danger btn-sm" type="button" data-toast-retry>Retry sync</button></div></div>
        <span class="nt-close"><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Dismiss notification"></button></span></div>
    </div>
  </div>
  </div>

  <!-- Templates the script clones. Errors have no timer: they stay until dismissed. -->
  <template data-toast-template="mention"><div class="toast nt-toast fade" data-bs-autohide="false" data-toast style="--nt-time:9s"><div class="toast-body"><span class="nt-avatar" aria-hidden="true" data-avatar></span><div class="flex-grow-1 nt-body"><p class="nt-title" data-title></p><p class="nt-text" data-text></p><p class="nt-meta" data-meta></p></div><span class="nt-close"><svg class="nt-ring" viewBox="0 0 36 36" aria-hidden="true"><circle class="nt-track" cx="18" cy="18" r="16"/><circle class="nt-time" cx="18" cy="18" r="16" pathLength="100"/></svg><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Dismiss notification"></button></span></div></div></template>
  <template data-toast-template="success"><div class="toast nt-toast nt-success fade" data-bs-autohide="false" data-toast style="--nt-time:6s"><div class="toast-body"><span class="nt-icon" aria-hidden="true"><svg><use href="#i-ok"/></svg></span><div class="flex-grow-1 nt-body"><p class="nt-title" data-title></p><p class="nt-text" data-text></p></div><span class="nt-close"><svg class="nt-ring" viewBox="0 0 36 36" aria-hidden="true"><circle class="nt-track" cx="18" cy="18" r="16"/><circle class="nt-time" cx="18" cy="18" r="16" pathLength="100"/></svg><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Dismiss notification"></button></span></div></div></template>
  <template data-toast-template="error"><div class="toast nt-toast nt-error fade" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="false" data-toast><div class="toast-body"><span class="nt-icon" aria-hidden="true"><svg><use href="#i-error"/></svg></span><div class="flex-grow-1 nt-body"><p class="nt-title" data-title></p><p class="nt-text" data-text></p><div class="d-flex gap-2 mt-2"><button class="btn btn-danger btn-sm" type="button" data-toast-retry>Retry sync</button></div></div><span class="nt-close"><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Dismiss notification"></button></span></div></div></template>
</div></div>

<div><span class="lbl">Static variants: info, success, warning and error</span>
<div class="row g-3">
  <div class="col-md-6"><div class="toast nt-toast nt-static show" data-bs-autohide="false">
    <div class="toast-body"><span class="nt-icon" aria-hidden="true"><svg><use href="#i-info"/></svg></span>
      <div class="flex-grow-1 nt-body"><p class="nt-title">Scheduled maintenance</p><p class="nt-text">Reports are read-only on Sunday from 02:00 to 03:00 UTC.</p></div>
      <span class="nt-close"><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Dismiss notification"></button></span></div></div></div>
  <div class="col-md-6"><div class="toast nt-toast nt-success nt-static show" data-bs-autohide="false">
    <div class="toast-body"><span class="nt-icon" aria-hidden="true"><svg><use href="#i-ok"/></svg></span>
      <div class="flex-grow-1 nt-body"><p class="nt-title">Payment received</p><p class="nt-text">$1,240.00 from Harbor Labs was added to your balance.</p></div>
      <span class="nt-close"><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Dismiss notification"></button></span></div></div></div>
  <div class="col-md-6"><div class="toast nt-toast nt-warning nt-static show" data-bs-autohide="false">
    <div class="toast-body"><span class="nt-icon" aria-hidden="true"><svg><use href="#i-warn"/></svg></span>
      <div class="flex-grow-1 nt-body"><p class="nt-title">Storage almost full</p><p class="nt-text">You’ve used 9.4 of 10 GB. Old exports are removed after 30 days.</p></div>
      <span class="nt-close"><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Dismiss notification"></button></span></div></div></div>
  <div class="col-md-6"><div class="toast nt-toast nt-error nt-static show" data-bs-autohide="false">
    <div class="toast-body"><span class="nt-icon" aria-hidden="true"><svg><use href="#i-error"/></svg></span>
      <div class="flex-grow-1 nt-body"><p class="nt-title">Card declined</p><p class="nt-text">The Visa ending 4418 was declined. Update it to keep your plan.</p></div>
      <span class="nt-close"><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Dismiss notification"></button></span></div></div></div>
</div></div>
CSS
/* Bootstrap .toast-container + .toast. Autohide is off: the ring around the close button is the timer.
   Its CSS animation runs for --nt-time and the script hides the toast on animationend, so hover and focus pause it. */
.nt-stack,.nt-toast{--nt-brand:#4f46e5;--nt-ink:#0f172a;--nt-text:#475569;--nt-muted:#5b6474;--nt-line:#e2e8f0}
.nt-stack{z-index:1090;width:min(412px,100%);--bs-toast-spacing:0}
.nt-toast{--nt-accent:var(--nt-brand);--nt-soft:#eef2ff;--nt-from:-12px;
  --bs-toast-spacing:0;--bs-toast-max-width:100%;--bs-toast-bg:#fff;--bs-toast-color:var(--nt-text);--bs-toast-border-color:var(--nt-line);--bs-toast-border-radius:16px;--bs-toast-font-size:.875rem;
  --bs-toast-box-shadow:0 20px 40px -20px rgba(15,23,42,.38),0 2px 6px rgba(15,23,42,.06);line-height:1.45}
.nt-toast.show{animation:nt-in .3s cubic-bezier(.2,.8,.2,1)}
.nt-stack.flex-column .nt-toast{--nt-from:12px}
.nt-toast .toast-body{display:flex;align-items:flex-start;gap:12px;padding:14px 12px 14px 14px}
.nt-success{--nt-accent:#15803d;--nt-soft:#dcfce7}
.nt-warning{--nt-accent:#b45309;--nt-soft:#fef3c7}
.nt-error{--nt-accent:#dc2626;--nt-soft:#fee2e2;--bs-toast-border-color:#fecaca}
.nt-icon,.nt-avatar{flex:none;display:grid;place-items:center;width:36px;height:36px;border-radius:50%;background:var(--nt-soft);color:var(--nt-accent)}
.nt-icon svg{width:19px;height:19px}
.nt-avatar{background:#ddd6fe;color:#4c1d95;font-size:.8125rem;font-weight:700;letter-spacing:.02em}
.nt-body{min-width:0}
.nt-title{margin:0;font-size:.9375rem;font-weight:600;color:var(--nt-ink)}
.nt-text{margin:2px 0 0}
.nt-meta{margin:6px 0 0;font-size:.75rem;color:var(--nt-muted)}
.nt-toast .btn-danger{--bs-btn-bg:#dc2626;--bs-btn-border-color:#dc2626;--bs-btn-hover-bg:#b91c1c;--bs-btn-hover-border-color:#b91c1c;--bs-btn-font-weight:600;--bs-btn-border-radius:9px;
  --bs-btn-focus-box-shadow:0 0 0 2px #fff,0 0 0 4px #4f46e5;height:32px}
/* Close button with the timer ring around it */
.nt-close{position:relative;flex:none;display:grid;place-items:center;width:34px;height:34px;margin:-4px -2px 0 0}
.nt-close .btn-close{--bs-btn-close-opacity:.65;--bs-btn-close-hover-opacity:1;--bs-btn-close-focus-shadow:0 0 0 2px #4f46e5;width:34px;height:34px;padding:0;border-radius:50%;background-size:10px}
.nt-close .btn-close:hover{background-color:#f1f5f9}
.nt-ring{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg);pointer-events:none}
.nt-ring circle{fill:none;stroke-width:2.5}
.nt-ring .nt-track{stroke:#e2e8f0}
.nt-ring .nt-time{stroke:var(--nt-accent);stroke-dasharray:100;stroke-dashoffset:0;stroke-linecap:round;animation:nt-time var(--nt-time,6s) linear forwards}
.nt-stack:hover .nt-time,.nt-stack:focus-within .nt-time{animation-play-state:paused}
@keyframes nt-in{from{transform:translateY(var(--nt-from)) scale(.97)}}
@keyframes nt-time{to{stroke-dashoffset:100}}
.nt-static{--bs-toast-box-shadow:0 1px 2px rgba(15,23,42,.05),0 12px 28px -20px rgba(15,23,42,.35)}
.nt-static.show{animation:none}
@media (prefers-reduced-motion:reduce){.nt-toast.show{animation:none}.nt-toast.fade{transition:none}}
JavaScript
(function () {
  var stack = document.querySelector('[data-toast-stack]');
  if (!stack) return;
  var MAX = 4, lastTrigger = null, n = 0;
  var POS = { tr: 'top-0 end-0 flex-column-reverse', tc: 'top-0 start-50 translate-middle-x flex-column-reverse', br: 'bottom-0 end-0 flex-column', bl: 'bottom-0 start-0 flex-column' };
  var content = {
    mention: [
      { avatar: 'LO', title: 'Leo Okafor replied to you', text: '“Pushed the new hero copy, take a look when you can.”', meta: 'Website launch · just now' },
      { avatar: 'MK', title: 'Mina Kovač mentioned you', text: '“@Sam the client approved option B.”', meta: 'Bramble rebrand · just now' }
    ],
    success: [{ title: 'Invoice INV-2042 sent', text: 'Harbor Labs will get it at [email protected].' }],
    error: [{ title: 'Google Drive sync failed', text: 'We couldn’t reach Drive. Your files are saved locally.' }]
  };

  function arm(toast) {
    var timer = toast.querySelector('.nt-time');
    if (timer) timer.addEventListener('animationend', function () { dismiss(toast); });
    var hadFocus = false, sibling = null;
    toast.addEventListener('hide.bs.toast', function () {
      toast.dataset.leaving = 'true';
      hadFocus = toast.contains(document.activeElement);
      sibling = toast.nextElementSibling || toast.previousElementSibling;
    });
    // Remove the hidden toast, and hand focus to the next toast or back to the button that opened it.
    toast.addEventListener('hidden.bs.toast', function () {
      toast.remove();
      if (!hadFocus) return;
      var target = (sibling && sibling.querySelector('button')) || lastTrigger;
      if (target) target.focus();
    });
  }

  function dismiss(toast) {
    if (toast.isConnected && !toast.dataset.leaving) bootstrap.Toast.getOrCreateInstance(toast).hide();
  }

  function show(type, c) {
    var tpl = document.querySelector('[data-toast-template="' + type + '"]');
    if (!tpl) return;
    var list = content[type];
    c = c || list[n++ % list.length];
    var toast = tpl.content.firstElementChild.cloneNode(true);
    ['avatar', 'title', 'text', 'meta'].forEach(function (k) {
      var el = toast.querySelector('[data-' + k + ']');
      if (el) el.textContent = c[k] || '';
    });
    stack.appendChild(toast);
    arm(toast);
    bootstrap.Toast.getOrCreateInstance(toast).show();
    var live = stack.querySelectorAll('[data-toast]:not([data-leaving])');
    if (live.length > MAX) dismiss(live[0]);
  }

  stack.querySelectorAll('[data-toast]').forEach(arm);

  document.addEventListener('click', function (e) {
    var btn = e.target.closest('[data-toast-show]');
    if (btn) { lastTrigger = btn; show(btn.dataset.toastShow); return; }
    var toast = e.target.closest('[data-toast]');
    if (toast && e.target.closest('[data-toast-retry]')) { dismiss(toast); show('success', { title: 'Drive is back in sync', text: '3 files uploaded to Northwind / Projects.' }); }
  });

  // Esc dismisses the toast that has focus.
  stack.addEventListener('keydown', function (e) {
    var toast = e.target.closest('[data-toast]');
    if (e.key === 'Escape' && toast) dismiss(toast);
  });

  var pos = document.querySelector('[data-toast-position]');
  if (pos) pos.addEventListener('change', function () {
    Object.keys(POS).forEach(function (k) { stack.classList.remove.apply(stack.classList, POS[k].split(' ')); });
    stack.classList.add.apply(stack.classList, POS[pos.value].split(' '));
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 12.5 4 4 8-9"/></symbol>
  <symbol id="i-error" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v6M12 17h.01"/></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.6 17.5A2 2 0 0 0 4.3 20.5h15.4a2 2 0 0 0 1.7-3L13.7 4.2a2 2 0 0 0-3.4 0z"/><path d="M12 9.5v4M12 17h.01"/></symbol>
  <symbol id="i-info" 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 11v5M12 8h.01"/></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>
</svg>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Live stack: auto-dismiss with a visible timer, paused on hover or focus</span>
<div class="stage relative flex h-[520px] flex-col overflow-hidden rounded-[18px] border border-[#d6dce5] bg-[#f8fafc] shadow-[0_1px_2px_rgba(15,23,42,.05),0_24px_48px_-28px_rgba(15,23,42,.3)] max-[600px]:h-[660px]">
  <div class="flex flex-wrap items-center gap-2.5 border-b border-[#e2e8f0] bg-white px-[18px] py-3.5"><b class="mr-auto text-[.95rem] text-[#0f172a]">Northwind · Projects</b>
    <span class="inline-flex items-center gap-2"><label class="text-[.8125rem] text-[#5b6474]" for="nt-pos">Position</label>
    <select class="h-9 cursor-pointer rounded-[10px] border border-[#cbd5e1] bg-white px-2.5 text-sm text-[#0f172a] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" id="nt-pos" data-toast-position>
      <option value="tr">Top right</option><option value="tc">Top centre</option><option value="br">Bottom right</option><option value="bl">Bottom left</option>
    </select></span>
    <button class="h-9 cursor-pointer rounded-[10px] border border-[#cbd5e1] bg-white px-3.5 text-sm font-semibold text-[#0f172a] hover:bg-[#f1f5f9] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" data-toast-show="mention">Get a mention</button>
    <button class="h-9 cursor-pointer rounded-[10px] border border-[#cbd5e1] bg-white px-3.5 text-sm font-semibold text-[#0f172a] hover:bg-[#f1f5f9] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" data-toast-show="success">Send invoice</button>
    <button class="h-9 cursor-pointer rounded-[10px] border border-[#cbd5e1] bg-white px-3.5 text-sm font-semibold text-[#0f172a] hover:bg-[#f1f5f9] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" data-toast-show="error">Sync Drive</button>
  </div>
  <div class="relative min-h-0 flex-1">
  <div class="grid gap-2.5 p-5" aria-hidden="true"><i class="flex h-14 items-center gap-3 rounded-xl border border-[#edf0f5] bg-white px-4 before:size-[30px] before:rounded-full before:bg-[#e2e8f0] after:h-[9px] after:w-[42%] after:rounded-[9px] after:bg-[#e7ebf1]"></i><i class="flex h-14 items-center gap-3 rounded-xl border border-[#edf0f5] bg-white px-4 before:size-[30px] before:rounded-full before:bg-[#e2e8f0] after:h-[9px] after:w-[42%] after:rounded-[9px] after:bg-[#e7ebf1]"></i><i class="flex h-14 items-center gap-3 rounded-xl border border-[#edf0f5] bg-white px-4 before:size-[30px] before:rounded-full before:bg-[#e2e8f0] after:h-[9px] after:w-[42%] after:rounded-[9px] after:bg-[#e7ebf1]"></i><i class="flex h-14 items-center gap-3 rounded-xl border border-[#edf0f5] bg-white px-4 before:size-[30px] before:rounded-full before:bg-[#e2e8f0] after:h-[9px] after:w-[42%] after:rounded-[9px] after:bg-[#e7ebf1]"></i><i class="flex h-14 items-center gap-3 rounded-xl border border-[#edf0f5] bg-white px-4 before:size-[30px] before:rounded-full before:bg-[#e2e8f0] after:h-[9px] after:w-[42%] after:rounded-[9px] after:bg-[#e7ebf1]"></i><i class="flex h-14 items-center gap-3 rounded-xl border border-[#edf0f5] bg-white px-4 before:size-[30px] before:rounded-full before:bg-[#e2e8f0] after:h-[9px] after:w-[42%] after:rounded-[9px] after:bg-[#e7ebf1]"></i></div>

  <div class="group/stack fixed top-4 right-4 z-[1090] flex w-[min(380px,calc(100%-32px))] flex-col-reverse gap-2 data-[pos=bl]:top-auto data-[pos=bl]:right-auto data-[pos=bl]:bottom-4 data-[pos=bl]:left-4 data-[pos=bl]:flex-col data-[pos=bl]:[--nt-from:12px] data-[pos=br]:top-auto data-[pos=br]:bottom-4 data-[pos=br]:flex-col data-[pos=br]:[--nt-from:12px] data-[pos=tc]:right-auto data-[pos=tc]:left-1/2 data-[pos=tc]:-translate-x-1/2 max-[480px]:right-3! max-[480px]:left-3! max-[480px]:w-auto max-[480px]:translate-x-0!" data-pos="tr" role="status" aria-live="polite" aria-atomic="false" aria-label="Notifications" data-toast-stack>
    <div class="relative flex items-start gap-3 rounded-2xl border bg-white py-3.5 pr-3 pl-3.5 text-sm leading-[1.45] text-[#475569] shadow-[0_20px_40px_-20px_rgba(15,23,42,.38),0_2px_6px_rgba(15,23,42,.06)] animate-toast-in data-[leaving]:animate-toast-out motion-reduce:animate-none motion-reduce:data-[leaving]:animate-none border-[#e2e8f0]" data-toast style="--nt-time:9s">
      <span class="grid size-9 flex-none place-items-center rounded-full bg-[#ddd6fe] text-[.8125rem] font-bold tracking-[.02em] text-[#4c1d95]" aria-hidden="true">PR</span>
      <div class="min-w-0 flex-1"><p class="m-0 text-[.9375rem] font-semibold text-[#0f172a]">Priya Raman mentioned you</p><p class="mt-0.5 mb-0">“@Sam can you check the Q4 pricing copy before Friday?”</p><p class="mt-1.5 mb-0 text-xs text-[#5b6474]">Atlas redesign · just now</p></div>
      <button class="relative -mt-1 -mr-0.5 grid size-[34px] flex-none cursor-pointer place-items-center rounded-full 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" data-toast-close aria-label="Dismiss notification"><svg class="pointer-events-none absolute inset-0 size-full -rotate-90" viewBox="0 0 36 36" aria-hidden="true"><circle class="stroke-[#e2e8f0]" fill="none" stroke-width="2.5" cx="18" cy="18" r="16"/><circle class="stroke-[#4f46e5] [stroke-dasharray:100] [stroke-linecap:round] animate-toast-time group-hover/stack:[animation-play-state:paused] group-focus-within/stack:[animation-play-state:paused]" fill="none" stroke-width="2.5" cx="18" cy="18" r="16" pathLength="100" data-timer/></svg><svg class="size-3.5" aria-hidden="true"><use href="#i-x"/></svg></button>
    </div>
    <div class="relative flex items-start gap-3 rounded-2xl border bg-white py-3.5 pr-3 pl-3.5 text-sm leading-[1.45] text-[#475569] shadow-[0_20px_40px_-20px_rgba(15,23,42,.38),0_2px_6px_rgba(15,23,42,.06)] animate-toast-in data-[leaving]:animate-toast-out motion-reduce:animate-none motion-reduce:data-[leaving]:animate-none border-[#e2e8f0]" data-toast style="--nt-time:7s">
      <span class="grid size-9 flex-none place-items-center rounded-full bg-[#dcfce7] text-[#15803d]" aria-hidden="true"><svg class="size-[19px]"><use href="#i-ok"/></svg></span>
      <div class="min-w-0 flex-1"><p class="m-0 text-[.9375rem] font-semibold text-[#0f172a]">Invoice INV-2041 sent</p><p class="mt-0.5 mb-0">Bramble & Co. will get it at [email protected].</p></div>
      <button class="relative -mt-1 -mr-0.5 grid size-[34px] flex-none cursor-pointer place-items-center rounded-full 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" data-toast-close aria-label="Dismiss notification"><svg class="pointer-events-none absolute inset-0 size-full -rotate-90" viewBox="0 0 36 36" aria-hidden="true"><circle class="stroke-[#e2e8f0]" fill="none" stroke-width="2.5" cx="18" cy="18" r="16"/><circle class="stroke-[#15803d] [stroke-dasharray:100] [stroke-linecap:round] animate-toast-time group-hover/stack:[animation-play-state:paused] group-focus-within/stack:[animation-play-state:paused]" fill="none" stroke-width="2.5" cx="18" cy="18" r="16" pathLength="100" data-timer/></svg><svg class="size-3.5" aria-hidden="true"><use href="#i-x"/></svg></button>
    </div>
    <div class="relative flex items-start gap-3 rounded-2xl border bg-white py-3.5 pr-3 pl-3.5 text-sm leading-[1.45] text-[#475569] shadow-[0_20px_40px_-20px_rgba(15,23,42,.38),0_2px_6px_rgba(15,23,42,.06)] animate-toast-in data-[leaving]:animate-toast-out motion-reduce:animate-none motion-reduce:data-[leaving]:animate-none border-[#fecaca]" role="alert" data-toast>
      <span class="grid size-9 flex-none place-items-center rounded-full bg-[#fee2e2] text-[#dc2626]" aria-hidden="true"><svg class="size-[19px]"><use href="#i-error"/></svg></span>
      <div class="min-w-0 flex-1"><p class="m-0 text-[.9375rem] font-semibold text-[#0f172a]">Google Drive sync failed</p><p class="mt-0.5 mb-0">We couldn’t reach Drive. Your 3 new files are saved locally.</p><div class="mt-2.5 flex flex-wrap gap-2"><button class="h-8 cursor-pointer rounded-[9px] border border-[#dc2626] bg-[#dc2626] px-3 text-[.8125rem] font-semibold text-white hover:bg-[#b91c1c] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" data-toast-retry>Retry sync</button></div></div>
      <button class="relative -mt-1 -mr-0.5 grid size-[34px] flex-none cursor-pointer place-items-center rounded-full 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" data-toast-close aria-label="Dismiss notification"><svg class="size-3.5" aria-hidden="true"><use href="#i-x"/></svg></button>
    </div>
  </div>
  </div>

  <!-- Templates the script clones. Errors have no timer: they stay until dismissed. -->
  <template data-toast-template="mention"><div class="relative flex items-start gap-3 rounded-2xl border bg-white py-3.5 pr-3 pl-3.5 text-sm leading-[1.45] text-[#475569] shadow-[0_20px_40px_-20px_rgba(15,23,42,.38),0_2px_6px_rgba(15,23,42,.06)] animate-toast-in data-[leaving]:animate-toast-out motion-reduce:animate-none motion-reduce:data-[leaving]:animate-none border-[#e2e8f0]" data-toast style="--nt-time:9s"><span class="grid size-9 flex-none place-items-center rounded-full bg-[#ddd6fe] text-[.8125rem] font-bold tracking-[.02em] text-[#4c1d95]" aria-hidden="true" data-avatar></span><div class="min-w-0 flex-1"><p class="m-0 text-[.9375rem] font-semibold text-[#0f172a]" data-title></p><p class="mt-0.5 mb-0" data-text></p><p class="mt-1.5 mb-0 text-xs text-[#5b6474]" data-meta></p></div><button class="relative -mt-1 -mr-0.5 grid size-[34px] flex-none cursor-pointer place-items-center rounded-full 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" data-toast-close aria-label="Dismiss notification"><svg class="pointer-events-none absolute inset-0 size-full -rotate-90" viewBox="0 0 36 36" aria-hidden="true"><circle class="stroke-[#e2e8f0]" fill="none" stroke-width="2.5" cx="18" cy="18" r="16"/><circle class="stroke-[#4f46e5] [stroke-dasharray:100] [stroke-linecap:round] animate-toast-time group-hover/stack:[animation-play-state:paused] group-focus-within/stack:[animation-play-state:paused]" fill="none" stroke-width="2.5" cx="18" cy="18" r="16" pathLength="100" data-timer/></svg><svg class="size-3.5" aria-hidden="true"><use href="#i-x"/></svg></button></div></template>
  <template data-toast-template="success"><div class="relative flex items-start gap-3 rounded-2xl border bg-white py-3.5 pr-3 pl-3.5 text-sm leading-[1.45] text-[#475569] shadow-[0_20px_40px_-20px_rgba(15,23,42,.38),0_2px_6px_rgba(15,23,42,.06)] animate-toast-in data-[leaving]:animate-toast-out motion-reduce:animate-none motion-reduce:data-[leaving]:animate-none border-[#e2e8f0]" data-toast style="--nt-time:6s"><span class="grid size-9 flex-none place-items-center rounded-full bg-[#dcfce7] text-[#15803d]" aria-hidden="true"><svg class="size-[19px]"><use href="#i-ok"/></svg></span><div class="min-w-0 flex-1"><p class="m-0 text-[.9375rem] font-semibold text-[#0f172a]" data-title></p><p class="mt-0.5 mb-0" data-text></p></div><button class="relative -mt-1 -mr-0.5 grid size-[34px] flex-none cursor-pointer place-items-center rounded-full 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" data-toast-close aria-label="Dismiss notification"><svg class="pointer-events-none absolute inset-0 size-full -rotate-90" viewBox="0 0 36 36" aria-hidden="true"><circle class="stroke-[#e2e8f0]" fill="none" stroke-width="2.5" cx="18" cy="18" r="16"/><circle class="stroke-[#15803d] [stroke-dasharray:100] [stroke-linecap:round] animate-toast-time group-hover/stack:[animation-play-state:paused] group-focus-within/stack:[animation-play-state:paused]" fill="none" stroke-width="2.5" cx="18" cy="18" r="16" pathLength="100" data-timer/></svg><svg class="size-3.5" aria-hidden="true"><use href="#i-x"/></svg></button></div></template>
  <template data-toast-template="error"><div class="relative flex items-start gap-3 rounded-2xl border bg-white py-3.5 pr-3 pl-3.5 text-sm leading-[1.45] text-[#475569] shadow-[0_20px_40px_-20px_rgba(15,23,42,.38),0_2px_6px_rgba(15,23,42,.06)] animate-toast-in data-[leaving]:animate-toast-out motion-reduce:animate-none motion-reduce:data-[leaving]:animate-none border-[#fecaca]" role="alert" data-toast><span class="grid size-9 flex-none place-items-center rounded-full bg-[#fee2e2] text-[#dc2626]" aria-hidden="true"><svg class="size-[19px]"><use href="#i-error"/></svg></span><div class="min-w-0 flex-1"><p class="m-0 text-[.9375rem] font-semibold text-[#0f172a]" data-title></p><p class="mt-0.5 mb-0" data-text></p><div class="mt-2.5 flex flex-wrap gap-2"><button class="h-8 cursor-pointer rounded-[9px] border border-[#dc2626] bg-[#dc2626] px-3 text-[.8125rem] font-semibold text-white hover:bg-[#b91c1c] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" data-toast-retry>Retry sync</button></div></div><button class="relative -mt-1 -mr-0.5 grid size-[34px] flex-none cursor-pointer place-items-center rounded-full 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" data-toast-close aria-label="Dismiss notification"><svg class="size-3.5" aria-hidden="true"><use href="#i-x"/></svg></button></div></template>
</div></div>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Static variants: info, success, warning and error</span>
<div class="grid grid-cols-2 gap-3.5 max-[760px]:grid-cols-1">
  <div class="relative flex items-start gap-3 rounded-2xl border bg-white py-3.5 pr-3 pl-3.5 text-sm leading-[1.45] text-[#475569] shadow-[0_1px_2px_rgba(15,23,42,.05),0_12px_28px_-20px_rgba(15,23,42,.35)] border-[#e2e8f0]" data-toast>
    <span class="grid size-9 flex-none place-items-center rounded-full bg-[#eef2ff] text-[#4f46e5]" aria-hidden="true"><svg class="size-[19px]"><use href="#i-info"/></svg></span>
    <div class="min-w-0 flex-1"><p class="m-0 text-[.9375rem] font-semibold text-[#0f172a]">Scheduled maintenance</p><p class="mt-0.5 mb-0">Reports are read-only on Sunday from 02:00 to 03:00 UTC.</p></div>
    <button class="relative -mt-1 -mr-0.5 grid size-[34px] flex-none cursor-pointer place-items-center rounded-full 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" data-toast-close aria-label="Dismiss notification"><svg class="size-3.5" aria-hidden="true"><use href="#i-x"/></svg></button>
  </div>
  <div class="relative flex items-start gap-3 rounded-2xl border bg-white py-3.5 pr-3 pl-3.5 text-sm leading-[1.45] text-[#475569] shadow-[0_1px_2px_rgba(15,23,42,.05),0_12px_28px_-20px_rgba(15,23,42,.35)] border-[#e2e8f0]" data-toast>
    <span class="grid size-9 flex-none place-items-center rounded-full bg-[#dcfce7] text-[#15803d]" aria-hidden="true"><svg class="size-[19px]"><use href="#i-ok"/></svg></span>
    <div class="min-w-0 flex-1"><p class="m-0 text-[.9375rem] font-semibold text-[#0f172a]">Payment received</p><p class="mt-0.5 mb-0">$1,240.00 from Harbor Labs was added to your balance.</p></div>
    <button class="relative -mt-1 -mr-0.5 grid size-[34px] flex-none cursor-pointer place-items-center rounded-full 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" data-toast-close aria-label="Dismiss notification"><svg class="size-3.5" aria-hidden="true"><use href="#i-x"/></svg></button>
  </div>
  <div class="relative flex items-start gap-3 rounded-2xl border bg-white py-3.5 pr-3 pl-3.5 text-sm leading-[1.45] text-[#475569] shadow-[0_1px_2px_rgba(15,23,42,.05),0_12px_28px_-20px_rgba(15,23,42,.35)] border-[#e2e8f0]" data-toast>
    <span class="grid size-9 flex-none place-items-center rounded-full bg-[#fef3c7] text-[#b45309]" aria-hidden="true"><svg class="size-[19px]"><use href="#i-warn"/></svg></span>
    <div class="min-w-0 flex-1"><p class="m-0 text-[.9375rem] font-semibold text-[#0f172a]">Storage almost full</p><p class="mt-0.5 mb-0">You’ve used 9.4 of 10 GB. Old exports are removed after 30 days.</p></div>
    <button class="relative -mt-1 -mr-0.5 grid size-[34px] flex-none cursor-pointer place-items-center rounded-full 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" data-toast-close aria-label="Dismiss notification"><svg class="size-3.5" aria-hidden="true"><use href="#i-x"/></svg></button>
  </div>
  <div class="relative flex items-start gap-3 rounded-2xl border bg-white py-3.5 pr-3 pl-3.5 text-sm leading-[1.45] text-[#475569] shadow-[0_1px_2px_rgba(15,23,42,.05),0_12px_28px_-20px_rgba(15,23,42,.35)] border-[#fecaca]" data-toast>
    <span class="grid size-9 flex-none place-items-center rounded-full bg-[#fee2e2] text-[#dc2626]" aria-hidden="true"><svg class="size-[19px]"><use href="#i-error"/></svg></span>
    <div class="min-w-0 flex-1"><p class="m-0 text-[.9375rem] font-semibold text-[#0f172a]">Card declined</p><p class="mt-0.5 mb-0">The Visa ending 4418 was declined. Update it to keep your plan.</p></div>
    <button class="relative -mt-1 -mr-0.5 grid size-[34px] flex-none cursor-pointer place-items-center rounded-full 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" data-toast-close aria-label="Dismiss notification"><svg class="size-3.5" aria-hidden="true"><use href="#i-x"/></svg></button>
  </div>
</div></div>
CSS
/* Entry and exit, and the ring timer: the script dismisses a toast on the timer's animationend,
   so pausing it on hover or focus pauses the countdown. */
@theme {
  --animate-toast-in: toast-in .3s cubic-bezier(.2,.8,.2,1);
  --animate-toast-out: toast-out .2s ease-in forwards;
  --animate-toast-time: toast-time var(--nt-time, 6s) linear forwards;
  @keyframes toast-in { from { opacity: 0; transform: translateY(var(--nt-from, -12px)) scale(.97); } }
  @keyframes toast-out { to { opacity: 0; transform: scale(.96); } }
  @keyframes toast-time { from { stroke-dashoffset: 0; } to { stroke-dashoffset: 100; } }
}
JavaScript
(function () {
  var stack = document.querySelector('[data-toast-stack]');
  if (!stack) return;
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
  var MAX = 4, lastTrigger = null, n = 0;
  var content = {
    mention: [
      { avatar: 'LO', title: 'Leo Okafor replied to you', text: '“Pushed the new hero copy, take a look when you can.”', meta: 'Website launch · just now' },
      { avatar: 'MK', title: 'Mina Kovač mentioned you', text: '“@Sam the client approved option B.”', meta: 'Bramble rebrand · just now' }
    ],
    success: [{ title: 'Invoice INV-2042 sent', text: 'Harbor Labs will get it at [email protected].' }],
    error: [{ title: 'Google Drive sync failed', text: 'We couldn’t reach Drive. Your files are saved locally.' }]
  };

  function arm(toast) {
    var timer = toast.querySelector('[data-timer]');
    if (timer) timer.addEventListener('animationend', function () { dismiss(toast); });
  }

  function dismiss(toast) {
    if (!toast.isConnected || toast.hasAttribute('data-leaving')) return;
    var hadFocus = toast.contains(document.activeElement);
    var sibling = toast.nextElementSibling || toast.previousElementSibling;
    toast.setAttribute('data-leaving', '');
    function done() {
      toast.remove();
      if (!hadFocus) return;
      var target = (sibling && sibling.matches('[data-toast]') && sibling.querySelector('button')) || lastTrigger;
      if (target) target.focus();
    }
    if (reduce.matches || getComputedStyle(toast).animationName === 'none') return done();
    toast.addEventListener('animationend', function (e) { if (e.target === toast) done(); });
  }

  function show(type, c) {
    var tpl = document.querySelector('[data-toast-template="' + type + '"]');
    if (!tpl) return;
    var list = content[type];
    c = c || list[n++ % list.length];
    var toast = tpl.content.firstElementChild.cloneNode(true);
    ['avatar', 'title', 'text', 'meta'].forEach(function (k) {
      var el = toast.querySelector('[data-' + k + ']');
      if (el) el.textContent = c[k] || '';
    });
    stack.appendChild(toast);
    arm(toast);
    var live = stack.querySelectorAll('[data-toast]:not([data-leaving])');
    if (live.length > MAX) dismiss(live[0]);
  }

  stack.querySelectorAll('[data-toast]').forEach(arm);

  document.addEventListener('click', function (e) {
    var btn = e.target.closest('[data-toast-show]');
    if (btn) { lastTrigger = btn; show(btn.dataset.toastShow); return; }
    var toast = e.target.closest('[data-toast]');
    if (!toast) return;
    if (e.target.closest('[data-toast-close]')) dismiss(toast);
    if (e.target.closest('[data-toast-retry]')) { dismiss(toast); show('success', { title: 'Drive is back in sync', text: '3 files uploaded to Northwind / Projects.' }); }
  });

  // Esc dismisses the toast that has focus.
  document.addEventListener('keydown', function (e) {
    var toast = e.key === 'Escape' && e.target.closest && e.target.closest('[data-toast]');
    if (toast) dismiss(toast);
  });

  var pos = document.querySelector('[data-toast-position]');
  if (pos) pos.addEventListener('change', function () { stack.dataset.pos = pos.value; });
})();

About this set

A live toast stack inside a small app window, plus four static toasts for info, success, warning and error. Three buttons raise a mention toast with the sender’s initials and the message, a success toast for a sent invoice and an error toast for a failed Google Drive sync with a Retry sync button. A position menu moves the stack between top right, top centre, bottom right and bottom left, and new toasts enter from the nearest edge. The timer is visible: a ring around each close button empties over the toast’s lifetime, which is set per toast with one custom property, and the script dismisses the toast when that CSS animation ends. Because the animation is the timer, pointing at the stack or tabbing into it pauses every countdown, so nobody loses a message they are reading. Error toasts have no ring and stay until closed. The stack keeps at most four toasts and drops the oldest, Esc closes the toast that has focus, and focus then moves to the next toast or back to the button that raised it. Use the static variants for in-page messages that should not move. The Bootstrap version is built on .toast-container, .toast and the Toast plugin with autohide turned off.

What’s included

  • Visible ring timer around the close button, set per toast with --nt-time
  • Hover or keyboard focus pauses every countdown in the stack
  • Four positions: top right, top centre, bottom right, bottom left
  • Errors use role="alert" and never auto-dismiss
  • At most four toasts; Esc closes the focused one
  • Bootstrap .toast-container and .toast with position utilities

Accessibility

The stack is a polite live region (role="status", aria-live="polite") named Notifications, and error toasts use role="alert" so they are announced at once. Every close button is named Dismiss notification and shows a 2px focus outline, and closing a toast moves focus to the next toast or back to its trigger. Hover and focus pause the timers. Under prefers-reduced-motion the slide in and out is switched off; the ring keeps emptying because it is the countdown.

Customise it

In plain CSS change --nt-brand, --nt-ink and --nt-text on .nt-stack, the --nt-accent and --nt-soft pair on .nt-success, .nt-warning and .nt-error, and --nt-time on each toast. In Bootstrap the same classes also set the --bs-toast-* variables; in Tailwind edit the hex values in the border-, bg-, text- and stroke- utilities and the --nt-time inline style.