← Feedback

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

  1. Paste the snippet just before </head>.
  2. Add data-achievement-unlocked to an element holding the achievement’s name. The badge, the small heading and the card are built around it.
  3. 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-labelThe small line above the name. Default “Achievement unlocked”.
data-iconText or an emoji on the badge instead of the star, such as 7.
data-colorColour of the badge, rays and sparks. Default #f5b301.
data-bgColour of the card. Default #1d1630.
data-loopLeave after a pause and unlock again.
data-holdHow 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.

1 · Snippet
<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>
2 · Your HTML
<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>