โ† Overlays

Inline Confirm

Press Delete and the button stretches open right where it is into “Are you sure? Yes / No”, with a thin line running down while you decide. No popup, no jump: answer and it shrinks back into the button.

  • Q3 report.pdf2.4 MB
  • Team photo.jpg3.1 MB
  • Voice memo.m4a860 KB

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-inline-confirm to your delete button.
  3. Do the real work on its confirm event, which fires when Yes is pressed.

Options

Add any of these attributes to the same element.

data-questionThe question. Default “Are you sure?”
data-yesThe yes button’s text. Default “Yes”.
data-noThe no button’s text. Default “No”.
data-doneText shown with a tick for a moment after Yes, such as “Deleted”.
data-timeoutTime before it quietly cancels itself. Default 5000 ms; 0 waits for ever.
data-colorThe warning colour. Default #dc2626.
data-autoplayTime it waits for an answer in the demo, in ms: it asks and answers by itself while in view, until someone touches it.

Focus moves to No, the safe choice, and back to the button afterwards. Esc, a click elsewhere or the timeout cancel it and fire a cancel event; the countdown pauses while the pointer is over it. The button’s own click handlers don’t run: use confirm instead. From code: animacious.inlineConfirm(button).

1 ยท Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Inline Confirm: press a delete button and it stretches open, right where
    // it is, into "Are you sure? Yes / No". No pops up, nothing jumps: answer
    // and it shrinks back into the button.
    // Usage:
    //   <button data-inline-confirm>Delete</button>
    // Optional: data-question (default "Are you sure?"), data-yes (default
    // "Yes"), data-no (default "No"), data-done (text shown for a moment after
    // Yes, such as "Deleted"), data-timeout (ms before it quietly cancels,
    // default 5000; 0 waits for ever), data-color (the warning colour, default
    // #dc2626), data-autoplay (ms between demo runs: it asks and answers by
    // itself while in view, until someone touches it).
    // Events: "confirm" on the button when Yes is pressed, "cancel" when No,
    // Esc, a click elsewhere or the timeout closes it.
    // Script use: animacious.inlineConfirm(button) opens it.
    var DEFAULTS = { question: 'Are you sure?', yes: 'Yes', no: 'No', timeout: 5000, color: '#dc2626', autoplay: 2400 };

    var css =
      '.icf{position:relative;display:inline-flex;vertical-align:middle;max-width:100%}' +
      '.icf-ask{position:absolute;left:0;top:0;display:none;align-items:center;gap:.4em;box-sizing:border-box;padding:0 .3em 0 .9em;border-radius:99px;white-space:nowrap;overflow:hidden;background:color-mix(in srgb,var(--icf) 9%,#fff);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--icf) 28%,transparent);color:color-mix(in srgb,var(--icf) 80%,#000)}' +
      '.icf-open .icf-ask{position:relative;display:inline-flex}' +
      '.icf-open>.icf-btn{position:absolute;left:0;top:0;pointer-events:none}' +
      '.icf-q{font-weight:600;margin-right:.15em}' +
      '.icf-yes,.icf-no{display:inline-grid;place-items:center;height:calc(100% - .5em);min-height:0;padding:0 .85em;border:0;border-radius:99px;font:inherit;font-weight:700;cursor:pointer}' +
      '.icf-yes{background:var(--icf);color:#fff}' +
      '.icf-no{background:#fff;color:#1a1625;box-shadow:0 0 0 1px rgba(0,0,0,.08)}' +
      '.icf-yes:focus-visible,.icf-no:focus-visible{outline:2px solid var(--icf);outline-offset:2px}' +
      '.icf-time{position:absolute;left:12px;right:12px;bottom:2px;height:2px;border-radius:2px;background:var(--icf);opacity:.35;transform-origin:0 50%}' +
      '.icf-done{display:inline-flex;align-items:center;gap:.4em;padding-right:.6em;font-weight:700}' +
      '.icf-done svg{width:1.05em;height:1.05em}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var TICK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function on(v) { return v !== undefined && v !== 'false'; }

    function make(tag, cls, text) {
      var n = document.createElement(tag);
      n.className = cls;
      if (text) n.textContent = text;
      return n;
    }

    function setup(btn) {
      var d = btn.dataset;
      var c = {
        btn: btn, open: false, timer: null,
        timeout: d.timeout !== undefined ? +d.timeout : DEFAULTS.timeout,
        deco: !!btn.closest('[aria-hidden="true"]')
      };
      btn._icf = c;
      var wrap = make('span', 'icf');
      wrap.style.setProperty('--icf', d.color || DEFAULTS.color);
      btn.parentNode.insertBefore(wrap, btn);
      wrap.appendChild(btn);
      btn.classList.add('icf-btn');
      var tag = c.deco ? 'span' : 'button';
      var ask = make('span', 'icf-ask');
      var q = make('span', 'icf-q', d.question || DEFAULTS.question);
      var yes = make(tag, 'icf-yes', d.yes || DEFAULTS.yes);
      var no = make(tag, 'icf-no', d.no || DEFAULTS.no);
      if (!c.deco) {
        yes.type = no.type = 'button';
        ask.setAttribute('role', 'group');
        ask.setAttribute('aria-label', q.textContent);
      }
      ask.appendChild(q); ask.appendChild(yes); ask.appendChild(no);
      var bar = make('span', 'icf-time');
      bar.setAttribute('aria-hidden', 'true');
      if (c.timeout > 0) ask.appendChild(bar);
      wrap.appendChild(ask);
      var done = make('span', 'icf-done');
      done.innerHTML = TICK;
      done.appendChild(document.createTextNode(d.done || ''));
      Object.assign(c, { wrap: wrap, ask: ask, q: q, yes: yes, no: no, bar: bar, done: done, doneText: d.done });

      btn.addEventListener('click', function (e) {
        if (c.open || c.deco) return;
        e.preventDefault();
        e.stopImmediatePropagation();
        if (e.isTrusted && c.stopAuto) c.stopAuto();
        open(c, e.isTrusted || e.detail === 0);
      }, true);
      yes.addEventListener('click', function (e) { if (e.isTrusted && c.stopAuto) c.stopAuto(); answer(c, true); });
      no.addEventListener('click', function (e) { if (e.isTrusted && c.stopAuto) c.stopAuto(); answer(c, false); });
      ask.addEventListener('keydown', function (e) { if (e.key === 'Escape') { e.stopPropagation(); answer(c, false); } });
      ask.addEventListener('pointerenter', function () { if (c.tAnim) c.tAnim.pause(); });
      ask.addEventListener('pointerleave', function () { if (c.tAnim && c.open) c.tAnim.resume(); });
      document.addEventListener('pointerdown', function (e) {
        if (c.open && e.isTrusted && !wrap.contains(e.target)) answer(c, false);
      });
      if (on(d.autoplay) && !reduced()) autoplay(c, +d.autoplay || DEFAULTS.autoplay);
      return c;
    }

    // Swap the wrapper's contents and stretch its width from the old to the new.
    function morph(c, show, from) {
      var wrap = c.wrap, w0 = wrap.offsetWidth;
      show();
      var w1 = wrap.offsetWidth;
      anime.utils.remove(wrap);
      if (reduced()) return;
      wrap.style.width = w0 + 'px';
      anime.animate(wrap, {
        width: w1, ease: anime.spring({ bounce: 0.25, duration: 520 }),
        onComplete: function () { wrap.style.width = ''; }
      });
      if (from) anime.animate(from, { opacity: [1, 0], scale: [1, 0.92], duration: 200, ease: 'out(2)' });
    }

    function open(c, byUser) {
      if (c.open) return;
      c.open = true;
      var h = c.btn.offsetHeight, cs = getComputedStyle(c.btn);
      c.ask.style.height = h + 'px';
      c.ask.style.font = cs.font;
      c.ask.style.borderRadius = cs.borderRadius;
      c.ask.replaceChildren(c.q, c.yes, c.no);
      if (c.timeout > 0) c.ask.appendChild(c.bar);
      c.focused = byUser && document.activeElement === c.btn;
      morph(c, function () { c.wrap.classList.add('icf-open'); }, c.btn);
      if (!c.deco) c.btn.setAttribute('aria-hidden', 'true');
      if (c.focused) c.no.focus();
      var parts = [c.q, c.yes, c.no];
      anime.utils.remove(parts);
      if (!reduced()) anime.animate(parts, { opacity: [0, 1], x: [10, 0], duration: 360, delay: anime.stagger(55, { start: 80 }), ease: 'out(3)' });
      if (c.timeout > 0) {
        c.tAnim = anime.animate(c.bar, { scaleX: [1, 0], duration: c.timeout, ease: 'linear', onComplete: function () { if (c.open) answer(c, false); } });
      }
    }

    function answer(c, yes) {
      if (!c.open) return;
      if (c.tAnim) { c.tAnim.pause(); c.tAnim = null; }
      var hadFocus = c.wrap.contains(document.activeElement);
      c.btn.dispatchEvent(new CustomEvent(yes ? 'confirm' : 'cancel', { bubbles: true }));
      if (yes && c.doneText) {
        c.ask.replaceChildren(c.done);
        morph(c, function () {}, null);
        if (!reduced()) anime.animate(c.done, { opacity: [0, 1], scale: [0.8, 1], duration: 400, ease: 'out(3)' });
        if (!c.deco) c.done.setAttribute('role', 'status');
        if (hadFocus) { c.ask.tabIndex = -1; c.ask.focus(); }
        clearTimeout(c.timer);
        c.timer = setTimeout(function () { close(c, hadFocus); }, 1400);
        return;
      }
      close(c, hadFocus);
    }

    function close(c, refocus) {
      c.open = false;
      morph(c, function () { c.wrap.classList.remove('icf-open'); }, null);
      c.btn.removeAttribute('aria-hidden');
      anime.utils.remove(c.btn);
      if (reduced()) anime.utils.set(c.btn, { opacity: 1, scale: 1 });
      else anime.animate(c.btn, { opacity: [0, 1], scale: [0.92, 1], duration: 300, delay: 80, ease: 'out(3)' });
      if (refocus) c.btn.focus();
    }

    // Demo mode: press the button, then answer Yes and No in turn.
    function autoplay(c, ms) {
      var timers = [], visible = false, live = true, turn = 0;
      function later(fn, t) { timers.push(setTimeout(fn, t)); }
      function press(node) { anime.animate(node, { scale: [1, 0.9, 1], duration: 240, ease: 'out(2)' }); }
      function run() {
        timers.forEach(clearTimeout); timers = [];
        if (!visible || !live) return;
        later(function () {
          press(c.btn);
          later(function () { open(c, false); }, 120);
          var pick = turn++ % 2 === 0 && c.doneText ? c.yes : c.no;
          later(function () { press(pick); }, 120 + ms);
          later(function () { answer(c, pick === c.yes); }, 240 + ms);
          later(run, 240 + ms + (pick === c.yes ? 1900 : 500));
        }, 900);
      }
      c.stopAuto = function () { live = false; timers.forEach(clearTimeout); timers = []; };
      new IntersectionObserver(function (entries) {
        visible = entries[entries.length - 1].isIntersecting;
        if (visible && !c.open) run();
        else if (!visible) timers.forEach(clearTimeout);
      }, { threshold: 0.5 }).observe(c.wrap);
    }

    function init() {
      document.querySelectorAll('[data-inline-confirm]').forEach(function (btn) {
        if (btn._icf) return;
        setup(btn);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.inlineConfirm = function (btn) { open(btn._icf || setup(btn), true); };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 ยท Your HTML
<button data-inline-confirm data-done="Deleted">Delete</button>

<script>
  document.addEventListener('confirm', e => {
    // e.target is the button: delete for real here
  });
</script>