Cookie Banner Slide
A cookie notice that waits a moment, slides up politely with its contents following just behind, and drops away once a choice is made. It remembers the answer, so nobody sees it twice, and it never steals focus.
We use cookies to keep things running and to see what people enjoy. This one’s a demo: it saves nothing.
How to use it
- Paste the snippet just before
</head>. - Add
data-cookie-banner-slideto the banner, ideally early in<body>so keyboard users reach it soon. - Mark its buttons
data-cookie-acceptanddata-cookie-reject. - Load analytics only when
localStoragesaysaccepted, or on itscookiechoiceevent.
Options
Add any of these attributes to the same element.
data-key | The localStorage key for the choice. Default cookie-choice. |
data-delay | Wait before it slides up. Default 800 ms. |
data-icon | On a child, e.g. a ๐ช: it wobbles in. |
data-contained | Keep it inside its parent and save nothing (for previews). |
The event says what was chosen: banner.addEventListener('cookiechoice', e => e.detail.choice). From code, animacious.cookieBanner(banner) shows it again, e.g. from a “Cookie settings” link.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Cookie Banner Slide: a cookie notice that waits a moment, slides up
// politely from the bottom with its contents following just behind, and
// drops away once a choice is made. The choice is remembered, so it only
// ever shows once.
// Usage:
// <div data-cookie-banner-slide aria-label="Cookies">
// <p>We use cookies to improve the site. <a href="/privacy">Privacy</a></p>
// <button data-cookie-reject>Reject</button>
// <button data-cookie-accept>Accept</button>
// </div>
// The choice ("accepted" or "rejected") is saved in localStorage under
// data-key and announced with a "cookiechoice" event on the banner
// (event.detail.choice), so you can load analytics only after "accepted".
// It never takes focus, so it doesn't interrupt anyone; put it early in
// the page so keyboard users reach it soon.
// Optional: data-key (default "cookie-choice"), data-delay (ms before it
// shows, default 800), data-icon on a child (it wobbles in, e.g. a ๐ช).
// Contained mode (for previews): data-contained keeps it inside its parent
// (give that position: relative) and saves nothing; there, data-loop (ms)
// brings it back after a while and data-autoplay (ms) presses Accept by
// itself while it's on screen.
// Script use: animacious.cookieBanner(el) shows it again.
var DEFAULTS = { key: 'cookie-choice', delay: 800, loop: 2600 };
// Layout first (so it's right even with reduced motion); the look sits in
// :where() so it's easy to restyle.
var css =
'[data-cookie-banner-slide]{position:fixed;left:16px;right:16px;bottom:16px;z-index:9000;margin:0 auto;max-width:720px;visibility:hidden}' +
'[data-cookie-banner-slide][data-contained]{position:absolute;left:12px;right:12px;bottom:12px;z-index:3}' +
':where([data-cookie-banner-slide]){display:flex;flex-wrap:wrap;align-items:center;gap:12px 14px;padding:16px 18px;border-radius:16px;' +
'background:#15131f;color:#fff;box-shadow:0 18px 50px rgba(10,10,25,.3);text-align:left}' +
':where([data-cookie-banner-slide]) > p{flex:1 1 260px;margin:0;font-size:15px;line-height:1.5}' +
':where([data-cookie-banner-slide]) a{color:inherit;text-decoration:underline;text-underline-offset:2px}' +
':where([data-cookie-banner-slide]) [data-icon]{display:inline-block;font-size:1.6em;line-height:1}' +
':where([data-cookie-banner-slide]) :is([data-cookie-accept],[data-cookie-reject]){font:inherit;font-size:14px;font-weight:600;padding:9px 18px;border-radius:99px;' +
'border:1px solid rgba(255,255,255,.28);background:transparent;color:inherit;cursor:pointer}' +
':where([data-cookie-banner-slide]) [data-cookie-accept]{border-color:#fff;background:#fff;color:#15131f}' +
':where([data-cookie-banner-slide]) :is([data-cookie-accept],[data-cookie-reject]):focus-visible{outline:2px solid #c4b5fd;outline-offset:2px}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function read(key) { try { return localStorage.getItem(key); } catch (e) { return null; } }
function save(key, v) { try { localStorage.setItem(key, v); } catch (e) { /* private mode */ } }
function setup(el) {
var d = el.dataset;
var b = { el: el, on: false, contained: d.contained !== undefined && d.contained !== 'false', timer: null, auto: null };
el._cbs = b;
if (!el.closest('[aria-hidden="true"]')) {
if (!el.getAttribute('role')) el.setAttribute('role', 'region');
if (!el.getAttribute('aria-label') && !el.getAttribute('aria-labelledby')) el.setAttribute('aria-label', 'Cookie consent');
}
el.addEventListener('click', function (e) {
var t = e.target.closest('[data-cookie-accept],[data-cookie-reject]');
if (t) choose(b, t.hasAttribute('data-cookie-accept') ? 'accepted' : 'rejected', t);
});
return b;
}
function show(b) {
var el = b.el;
if (b.on) return;
b.on = true;
clearTimeout(b.timer);
el.style.visibility = 'visible';
var parts = Array.from(el.children);
var icon = el.querySelector('[data-icon]');
anime.utils.remove([el].concat(parts));
if (reduced()) { anime.utils.set([el].concat(parts), { y: 0, opacity: 1 }); return; }
var drop = el.offsetHeight + 40;
anime.animate(el, { y: [drop, 0], opacity: { to: [0, 1], duration: 200, ease: 'linear' }, ease: anime.spring({ bounce: 0.22, duration: 620 }) });
anime.animate(parts, { opacity: [0, 1], y: [14, 0], duration: 520, delay: anime.stagger(70, { start: 140 }), ease: 'out(3)' });
if (icon) anime.animate(icon, { rotate: [-30, 14, -6, 0], scale: [0.4, 1.15, 1], duration: 900, delay: 220, ease: 'out(2)' });
if (b.auto) {
clearTimeout(b.autoTimer);
b.autoTimer = setTimeout(function () {
var a = el.querySelector('[data-cookie-accept]');
if (b.on && a) choose(b, 'accepted', a);
}, b.auto);
}
}
function choose(b, choice, btn) {
var el = b.el, d = el.dataset;
if (!b.on) return;
b.on = false;
clearTimeout(b.autoTimer);
if (!b.contained) save(d.key || DEFAULTS.key, choice);
el.dispatchEvent(new CustomEvent('cookiechoice', { bubbles: true, detail: { choice: choice } }));
// Leave the keyboard somewhere sensible when the banner goes.
if (el.contains(document.activeElement)) document.activeElement.blur();
var gone = function () {
if (b.on) return;
el.style.visibility = 'hidden';
var loop = d.loop;
if (b.contained && loop !== undefined && loop !== 'false') b.timer = setTimeout(function () { show(b); }, +loop || DEFAULTS.loop);
};
anime.utils.remove(el);
if (reduced()) return gone();
// The pressed button gives a little nod, the banner lifts a touch, then drops away.
if (btn) anime.animate(btn, { scale: [1, 0.92, 1], duration: 260, ease: 'out(2)' });
var tl = anime.createTimeline({ onComplete: gone });
tl.add(el, { y: -8, duration: 180, ease: 'out(2)' }, 120)
.add(el, { y: el.offsetHeight + 40, opacity: { to: 0, duration: 260, delay: 140 }, duration: 420, ease: 'in(3)' });
}
function init() {
var rm = reduced();
document.querySelectorAll('[data-cookie-banner-slide]').forEach(function (el) {
if (el._cbs) return;
var b = setup(el), d = el.dataset;
var delay = d.delay !== undefined ? +d.delay : DEFAULTS.delay;
if (!b.contained) {
// Already chosen on an earlier visit: stay away.
if (read(d.key || DEFAULTS.key)) return;
b.timer = setTimeout(function () { show(b); }, rm ? 0 : delay);
return;
}
if (d.autoplay !== undefined && d.autoplay !== 'false' && !rm) b.auto = +d.autoplay || 1800;
// Contained: wait until it's on screen, so the entrance is seen.
var io = new IntersectionObserver(function (entries) {
var e = entries[0];
if (!e.isIntersecting) return;
if (!b.auto) io.disconnect();
if (!b.on) { clearTimeout(b.timer); b.timer = setTimeout(function () { show(b); }, rm ? 0 : delay); }
}, { threshold: 0.5 });
io.observe(el.parentNode);
});
}
window.animacious = window.animacious || {};
window.animacious.cookieBanner = function (el) { show(el._cbs || setup(el)); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-cookie-banner-slide aria-label="Cookies">
<p>We use cookies to improve the site.
<a href="/privacy">Privacy</a></p>
<button data-cookie-reject>Reject</button>
<button data-cookie-accept>Accept</button>
</div>
<script>
if (localStorage.getItem('cookie-choice') === 'accepted') {
// load analytics
}
</script>