Component Library

← Back to gallery

Undo snackbar

Category: Feedback · Tags: snackbar, undo, archive, status, auto-hide

Drafts

  • Weekly report draft
  • Client onboarding checklist
  • Team offsite agenda

How to use it

Use after a reversible action that you have already carried out -- archiving, moving, deleting into a bin -- to offer a quick undo instead of asking for confirmation first. Replace the list with your own items and call your real archive/restore code where the script hides and re-shows the item; keep the delay long enough to read and reach the button. Do not use it for irreversible actions (ask first in a dialog) or for messages without an action (use a toast).

Accessibility

The bar lives inside a role="status" region that is on the page from the start, so "Weekly report draft archived." and its Undo button are announced politely without focus being moved to them. Archiving moves focus to the next item's Archive button (or the list heading), never into the bar; Undo is a native <button> reached with Tab, and the 8-second auto-hide pauses while the pointer or keyboard focus is inside the bar. Bar text white on #0f172a is 17.9:1 and the Undo text #bfdbfe on #0f172a is 12.6:1.

Source (copy-ready)

<section class="dcl-undosnack" aria-labelledby="dcl-undosnack-title">
  <h3 class="dcl-undosnack__title" id="dcl-undosnack-title">Drafts</h3>
  <ul class="dcl-undosnack__list">
    <li class="dcl-undosnack__item"><span class="dcl-undosnack__name">Weekly report draft</span><button type="button" class="dcl-undosnack__archive">Archive</button></li>
    <li class="dcl-undosnack__item"><span class="dcl-undosnack__name">Client onboarding checklist</span><button type="button" class="dcl-undosnack__archive">Archive</button></li>
    <li class="dcl-undosnack__item"><span class="dcl-undosnack__name">Team offsite agenda</span><button type="button" class="dcl-undosnack__archive">Archive</button></li>
  </ul>
  <div class="dcl-undosnack__live" role="status">
    <div class="dcl-undosnack__bar" hidden>
      <p class="dcl-undosnack__message"></p>
      <button type="button" class="dcl-undosnack__undo">Undo</button>
    </div>
  </div>
</section>
<style>
.dcl-undosnack { max-width: 28rem; font: inherit; color: #0f172a; background-color: #ffffff; }
.dcl-undosnack__title { margin: 0 0 0.5rem; font-size: 1rem; color: #0f172a; background-color: #ffffff; }
.dcl-undosnack__title:focus-visible { outline: 2px solid #1d4ed8; outline-offset: 2px; }
.dcl-undosnack__list { margin: 0; padding: 0; list-style: none; }
.dcl-undosnack__item { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 0.5rem 0; border-bottom: 1px solid #e2e8f0; }
.dcl-undosnack__item[hidden] { display: none; }
.dcl-undosnack__name { min-width: 0; overflow-wrap: anywhere; color: #0f172a; background-color: #ffffff; }
.dcl-undosnack__archive { padding: 0.25rem 0.75rem; border: 1px solid #94a3b8; border-radius: 0.375rem; font: inherit; cursor: pointer; color: #334155; background-color: #ffffff; }
.dcl-undosnack__archive:hover { color: #0f172a; background-color: #f1f5f9; }
.dcl-undosnack__archive:focus-visible { outline: 2px solid #1d4ed8; outline-offset: 2px; }
.dcl-undosnack__live { margin-top: 0.75rem; }
.dcl-undosnack__bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem 1rem; padding: 0.625rem 0.75rem 0.625rem 1rem; border-radius: 0.375rem; color: #ffffff; background-color: #0f172a; }
.dcl-undosnack__bar[hidden] { display: none; }
.dcl-undosnack__message { min-width: 0; margin: 0; overflow-wrap: anywhere; color: #ffffff; background-color: #0f172a; }
.dcl-undosnack__undo { padding: 0.25rem 0.625rem; border: none; border-radius: 0.25rem; font: inherit; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; cursor: pointer; color: #bfdbfe; background-color: #0f172a; }
.dcl-undosnack__undo:hover { color: #ffffff; background-color: #334155; }
.dcl-undosnack__undo:focus-visible { outline: 2px solid #bfdbfe; outline-offset: 2px; }
</style>
<script>
(function () {
  document.querySelectorAll(".dcl-undosnack").forEach(function (root) {
    if (root.dataset.dclBound) { return; }
    root.dataset.dclBound = "true";
    var bar = root.querySelector(".dcl-undosnack__bar");
    var message = root.querySelector(".dcl-undosnack__message");
    var undo = root.querySelector(".dcl-undosnack__undo");
    var heading = root.querySelector(".dcl-undosnack__title");
    var lastItem = null;
    var timer = null;
    var paused = false;

    function hide() {
      window.clearTimeout(timer);
      if (bar) { bar.hidden = true; }
      if (message) { message.textContent = ""; }
      lastItem = null;
    }
    function schedule() {
      window.clearTimeout(timer);
      if (!paused) { timer = window.setTimeout(hide, 8000); }
    }
    function visibleArchives() {
      return Array.prototype.filter.call(root.querySelectorAll(".dcl-undosnack__archive"), function (button) {
        var item = button.closest(".dcl-undosnack__item");
        return !item || !item.hidden;
      });
    }

    root.addEventListener("click", function (event) {
      var archive = event.target.closest(".dcl-undosnack__archive");
      if (!archive || !root.contains(archive)) { return; }
      var item = archive.closest(".dcl-undosnack__item");
      if (!item) { return; }
      var buttons = visibleArchives();
      var index = buttons.indexOf(archive);
      var nameNode = item.querySelector(".dcl-undosnack__name");
      item.hidden = true;
      lastItem = item;
      buttons.splice(index, 1);
      var next = buttons[index] || buttons[index - 1];
      if (next) {
        next.focus();
      } else if (heading) {
        heading.tabIndex = -1;
        heading.focus();
      }
      if (bar && message) {
        bar.hidden = false;
        message.textContent = (nameNode ? nameNode.textContent.trim() : "Item") + " archived.";
        schedule();
      }
    });

    if (undo) {
      undo.addEventListener("click", function () {
        var item = lastItem;
        paused = false;
        hide();
        if (item) {
          item.hidden = false;
          var button = item.querySelector(".dcl-undosnack__archive");
          if (button) { button.focus(); }
        }
      });
    }
    if (bar) {
      bar.addEventListener("mouseenter", function () { paused = true; window.clearTimeout(timer); });
      bar.addEventListener("mouseleave", function () { paused = false; if (!bar.hidden) { schedule(); } });
      bar.addEventListener("focusin", function () { paused = true; window.clearTimeout(timer); });
      bar.addEventListener("focusout", function (event) {
        if (bar.contains(event.relatedTarget)) { return; }
        paused = false;
        if (!bar.hidden) { schedule(); }
      });
    }
  });
})();
</script>

Was this useful? Send feedback.