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
- Paste the snippet just before
</head>. - Add
data-inline-confirmto your delete button. - Do the real work on its
confirmevent, which fires when Yes is pressed.
Options
Add any of these attributes to the same element.
data-question | The question. Default “Are you sure?” |
data-yes | The yes button’s text. Default “Yes”. |
data-no | The no button’s text. Default “No”. |
data-done | Text shown with a tick for a moment after Yes, such as “Deleted”. |
data-timeout | Time before it quietly cancels itself. Default 5000 ms; 0 waits for ever. |
data-color | The warning colour. Default #dc2626. |
data-autoplay | Time 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).
<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><button data-inline-confirm data-done="Deleted">Delete</button>
<script>
document.addEventListener('confirm', e => {
// e.target is the button: delete for real here
});
</script>