Achievement Unlocked
A game-style toast pops up, its badge spins in edge-on and lands face up with a burst of light rays and sparks. A shine sweeps over the medal and across the card while the achievement’s name slides in beside it. Size the whole thing with font-size.
Seven-Day Scholar
How to use it
- Paste the snippet just before
</head>. - Add
data-achievement-unlockedto an element holding the achievement’s name. The badge, the small heading and the card are built around it. - It plays when it scrolls into view. Add it to the page when the achievement is earned, or call
animacious.achievementUnlocked(el)to play it again.
Options
Add any of these attributes to the same element.
data-label | The small line above the name. Default “Achievement unlocked”. |
data-icon | Text or an emoji on the badge instead of the star, such as 7. |
data-color | Colour of the badge, rays and sparks. Default #f5b301. |
data-bg | Colour of the card. Default #1d1630. |
data-loop | Leave after a pause and unlock again. |
data-hold | How long it shows before leaving, when looping. Default 2200 ms. |
The card is a polite status region, so screen readers announce the unlock. With reduced motion the finished card simply shows.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Achievement Unlocked: a game-style toast. The card pops up, its badge
// spins in edge-on and lands with a burst of light rays and sparks, a shine
// sweeps across the medal and the card, and the title slides in beside it.
// Usage: <div data-achievement-unlocked>Early Bird</div>
// (the text is the achievement's name; size it all with font-size)
// Optional: data-label (the small line above, default "Achievement
// unlocked"), data-icon (text or an emoji on the badge instead of the
// star), data-color (badge colour, default #f5b301), data-bg (card colour,
// default #1d1630), data-loop (leave after a pause and unlock again),
// data-hold (ms it shows before leaving when looping, default 2200).
// Script use: animacious.achievementUnlocked(el) plays it again.
var DEFAULTS = { label: 'Achievement unlocked', color: '#f5b301', bg: '#1d1630', hold: 2200 };
var RAYS = 12, SPARKS = 10;
var STAR = '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M12 2.6l2.8 5.9 6.4.8-4.7 4.4 1.2 6.4L12 17l-5.7 3.1 1.2-6.4-4.7-4.4 6.4-.8z"/></svg>';
var css =
'.ach{position:relative;display:inline-flex;align-items:center;gap:.8em;padding:.6em 1.3em .6em .6em;border-radius:999px;' +
'background:var(--ach-bg);color:#fff;box-shadow:0 .7em 1.8em -.6em rgba(20,10,40,.55);text-align:left;line-height:1.2;max-width:100%}' +
'.ach-clip{position:absolute;inset:0;border-radius:inherit;overflow:hidden;pointer-events:none}' +
'.ach-gleam{position:absolute;top:-20%;bottom:-20%;left:0;width:35%;background:linear-gradient(100deg,transparent,rgba(255,255,255,.22),transparent)}' +
'.ach-medal{position:relative;flex:none;width:3em;height:3em;perspective:16em}' +
'.ach-glow{position:absolute;inset:-.9em;border-radius:50%;background:radial-gradient(closest-side,color-mix(in srgb,var(--ach-c) 75%,#fff),transparent);opacity:0}' +
'.ach-burst{position:absolute;left:50%;top:50%;width:0;height:0;opacity:0}' +
'.ach-ray{position:absolute;left:-.11em;top:-2.6em;width:.22em;height:1.05em;border-radius:.11em;background:linear-gradient(var(--ach-c),transparent);transform-origin:50% 2.6em}' +
'.ach-spark{position:absolute;left:-.12em;top:-.12em;width:.24em;height:.24em;border-radius:50%;background:color-mix(in srgb,var(--ach-c) 55%,#fff);opacity:0}' +
'.ach-badge{position:absolute;inset:0;display:grid;place-items:center;border-radius:50%;overflow:hidden;' +
'background:radial-gradient(circle at 35% 30%,color-mix(in srgb,var(--ach-c) 45%,#fff),var(--ach-c) 55%,color-mix(in srgb,var(--ach-c) 70%,#000));' +
'box-shadow:inset 0 0 0 .14em color-mix(in srgb,var(--ach-c) 55%,#fff),inset 0 -.2em .4em rgba(0,0,0,.25);color:#fff}' +
'.ach-badge svg{width:58%;height:58%;filter:drop-shadow(0 .06em .06em rgba(0,0,0,.3))}' +
'.ach-icon{font-size:1.35em;font-weight:800;text-shadow:0 .05em .08em rgba(0,0,0,.3)}' +
'.ach-shine{position:absolute;top:-10%;bottom:-10%;left:0;width:45%;background:linear-gradient(100deg,transparent,rgba(255,255,255,.75),transparent)}' +
'.ach-text{position:relative;display:grid;gap:.15em;min-width:0}' +
'.ach-kicker{font-size:.68em;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:color-mix(in srgb,var(--ach-c) 75%,#fff)}' +
'.ach-name{font-size:1.15em;font-weight:800;letter-spacing:-.01em;overflow-wrap:anywhere}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function esc(t) { var s = document.createElement('span'); s.textContent = t; return s.innerHTML; }
function setup(el) {
var d = el.dataset;
var name = el.textContent.trim();
el.classList.add('ach');
el.style.setProperty('--ach-c', d.color || DEFAULTS.color);
el.style.setProperty('--ach-bg', d.bg || DEFAULTS.bg);
el.setAttribute('role', 'status');
var rays = '', sparks = '';
for (var i = 0; i < RAYS; i++) rays += '<span class="ach-ray" style="transform:rotate(' + (i * 360 / RAYS) + 'deg)"></span>';
for (var j = 0; j < SPARKS; j++) sparks += '<span class="ach-spark"></span>';
el.innerHTML =
'<span class="ach-clip" aria-hidden="true"><span class="ach-gleam"></span></span>' +
'<span class="ach-medal" aria-hidden="true">' +
'<span class="ach-glow"></span><span class="ach-burst">' + rays + sparks + '</span>' +
'<span class="ach-badge">' + (d.icon ? '<span class="ach-icon">' + esc(d.icon) + '</span>' : STAR) + '<span class="ach-shine"></span></span>' +
'</span>' +
'<span class="ach-text"><span class="ach-kicker">' + esc(d.label || DEFAULTS.label) + '</span>' +
'<span class="ach-name">' + esc(name) + '</span></span>';
el._ach = {
gleam: el.querySelector('.ach-gleam'), glow: el.querySelector('.ach-glow'),
burst: el.querySelector('.ach-burst'), badge: el.querySelector('.ach-badge'),
shine: el.querySelector('.ach-shine'), sparks: el.querySelectorAll('.ach-spark'),
kicker: el.querySelector('.ach-kicker'), name: el.querySelector('.ach-name'),
tl: null, timer: null, run: 0
};
anime.utils.set(el._ach.gleam, { x: '-120%', skewX: -20 });
anime.utils.set(el._ach.shine, { x: '-150%', skewX: -18 });
if (!reduced()) anime.utils.set(el, { opacity: 0 });
}
function play(el) {
var s = el._ach, d = el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
var run = ++s.run;
clearTimeout(s.timer);
if (s.tl) s.tl.pause();
anime.utils.set(s.burst, { opacity: 0, scale: 0.4, rotate: 0 });
anime.utils.set(s.glow, { opacity: 0, scale: 0.5 });
anime.utils.set(s.sparks, { x: 0, y: 0, opacity: 0, scale: 1 });
anime.utils.set([s.kicker, s.name], { opacity: 0, x: '-0.6em' });
var tl = s.tl = anime.createTimeline({ autoplay: true });
// The card pops up.
tl.add(el, { opacity: [0, 1], scale: [0.7, 1], y: ['0.8em', '0em'], duration: 520, ease: 'outBack(1.7)' }, 0);
// The badge spins in edge-on and lands face up.
tl.add(s.badge, { rotateY: [-720, 0], scale: [0.2, 1], duration: 1150, ease: 'out(4)' }, 80);
// Light bursts out behind it.
var hit = 820;
tl.add(s.glow, { opacity: [0, 0.9, 0], scale: [0.5, 1.5], duration: 1100, ease: 'out(2)' }, hit);
tl.add(s.burst, { opacity: [{ to: 1, duration: 150 }, { to: 0, duration: 900, delay: 350 }], scale: [0.4, 1.35], rotate: 40, duration: 1400, ease: 'out(3)' }, hit);
s.sparks.forEach(function (sp, i) {
var a = (i / SPARKS) * Math.PI * 2 + anime.utils.random(-0.25, 0.25, 2);
var r = anime.utils.random(2.4, 3.6, 2);
tl.add(sp, {
x: ['0em', Math.cos(a) * r + 'em'], y: ['0em', Math.sin(a) * r + 'em'],
opacity: [{ to: 1, duration: 80 }, { to: 0, duration: 520, delay: 200 }], scale: [1, 0.3],
duration: 800, ease: 'out(3)'
}, hit + anime.utils.random(0, 80));
});
// A shine sweeps over the medal, then across the card.
tl.add(s.shine, { x: ['-150%', '260%'], duration: 700, ease: 'inOut(2)' }, hit + 250);
tl.add(s.gleam, { x: ['-120%', '420%'], duration: 1100, ease: 'inOut(2)' }, hit + 380);
// The title slides in.
tl.add(s.kicker, { opacity: 1, x: '0em', duration: 450, ease: 'out(3)' }, 380);
tl.add(s.name, { opacity: 1, x: '0em', duration: 500, ease: 'out(3)' }, 500);
if (!loop) return;
s.timer = setTimeout(function () {
if (run !== s.run) return;
anime.animate(el, {
opacity: 0, y: '-0.6em', scale: 0.96, duration: 420, ease: 'in(2)',
onComplete: function () { if (run === s.run) s.timer = setTimeout(function () { play(el); }, 500); }
});
}, 2000 + (+d.hold || DEFAULTS.hold));
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
play(e.target);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-achievement-unlocked]').forEach(function (el) {
if (el._ach) return;
setup(el);
if (!reduced()) io.observe(el);
});
}
window.animacious = window.animacious || {};
window.animacious.achievementUnlocked = function (el) { if (el._ach && !reduced()) play(el); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-achievement-unlocked>Early Bird</div>
<!-- your own wording and badge: -->
<div data-achievement-unlocked data-label="New badge" data-icon="7" data-color="#22c55e">Seven-Day Scholar</div>