← SVG & icons

Padlock Unlock

The keyhole clicks, the lock gives a little jiggle, then the shackle lifts and the padlock springs open with a bounce. Lock it again and the shackle swings back and drops home with a thud. Tap it to toggle.

Tap the padlock to lock or unlock it

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-padlock-unlock to an empty <span> or <button>. Size it with font-size and colour it with color.

Options

Add any of these attributes to the same element.

data-loopUnlock, hold, lock and repeat, until someone taps it.
data-holdHow long it stays open or shut, when looping. Default 1600 ms.
data-lockedStay locked until tapped, instead of unlocking when scrolled into view.
data-open-colorBody colour once it’s open, e.g. #16a34a.
data-shackleShackle colour. Default #a1a1aa.
data-delayWait before it unlocks.

It unlocks when scrolled into view. Its aria-label says whether it’s locked or unlocked. Listen for the toggle event (e.detail.open), or call animacious.padlock(el) to toggle it from code. With reduced motion it simply shows open.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Padlock Unlock: the keyhole clicks, the shackle lifts and the padlock springs
    // open with a little bounce. Lock it again and the shackle drops back in
    // with a thud. It unlocks when scrolled into view; tap it to toggle.
    // Usage: <span data-padlock-unlock></span>   (size with font-size, colour with color)
    // Optional: data-loop (unlock, hold, lock and repeat), data-hold (ms it stays
    // open or shut when looping, default 1600), data-delay (ms before it starts),
    // data-locked (stay locked until tapped), data-shackle (shackle colour,
    // default #a1a1aa), data-open-color (body colour once open, e.g. #22c55e).
    // Listen for the "toggle" event: e.detail.open is true or false.
    // Script use: animacious.padlock(el) toggles it from code.
    var DEFAULTS = { hold: 1600, shackle: '#a1a1aa' };
    var NS = 'http://www.w3.org/2000/svg';

    var css =
      '.pdl{display:inline-block;line-height:0;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
      '.pdl svg{display:block;width:1em;height:1.3em;overflow:visible}' +
      '.pdl-shackle{transform-origin:44px 44px}' +
      '.pdl-body{transform-origin:32px 72px}' +
      '.pdl-case{transition:fill .45s ease}' +
      '.pdl-key{transform-origin:32px 54px}' +
      '.pdl-ray{transform-origin:16px 26px;opacity:0}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function build(el) {
      var shackle = el.dataset.shackle || DEFAULTS.shackle;
      var svg = document.createElementNS(NS, 'svg');
      svg.setAttribute('viewBox', '0 -6 64 84');
      svg.setAttribute('aria-hidden', 'true');
      svg.innerHTML =
        // The shackle sits behind the body; its left leg is the short, free one.
        '<path class="pdl-shackle" d="M20 41 V26 a12 12 0 0 1 24 0 V46" fill="none" stroke="' + shackle + '" stroke-width="7" stroke-linecap="round"/>' +
        '<g class="pdl-rays" stroke="currentColor" stroke-width="2.6" stroke-linecap="round">' +
        '<path class="pdl-ray" d="M9 22 L4 19"/><path class="pdl-ray" d="M9 30 L3 31"/><path class="pdl-ray" d="M13 15 L10 10"/></g>' +
        '<g class="pdl-body"><rect class="pdl-case" x="8" y="38" width="48" height="34" rx="9" fill="currentColor"/>' +
        '<rect x="8" y="38" width="48" height="8" rx="4" fill="#fff" opacity=".14"/>' +
        '<g class="pdl-key" fill="#fff" opacity=".92"><circle cx="32" cy="52" r="4.6"/><rect x="30" y="53" width="4" height="9" rx="2"/></g></g>';
      el.appendChild(svg);
      return {
        el: el, open: false,
        shackle: svg.querySelector('.pdl-shackle'),
        body: svg.querySelector('.pdl-body'),
        caseEl: svg.querySelector('.pdl-case'),
        key: svg.querySelector('.pdl-key'),
        rays: svg.querySelectorAll('.pdl-ray'),
        color: el.dataset.openColor
      };
    }

    function label(s) {
      s.el.setAttribute('aria-label', s.open ? 'Unlocked' : 'Locked');
    }

    function place(s) {
      anime.utils.set(s.shackle, { y: s.open ? -10 : 0, rotate: s.open ? 30 : 0 });
      if (s.color) s.caseEl.style.fill = s.open ? s.color : '';
    }

    function unlock(s) {
      s.open = true;
      label(s);
      s.el.dispatchEvent(new CustomEvent('toggle', { bubbles: true, detail: { open: true } }));
      if (reduced()) { place(s); return; }
      var tl = anime.createTimeline();
      // Click the keyhole, jiggle the lock, then the shackle pops up and swings.
      tl.add(s.key, { scale: [1, 0.8, 1], duration: 320, ease: 'inOut(2)' })
        .add(s.body, { x: [0, -2, 2, -1, 0], duration: 260, ease: 'linear' }, '-=60')
        .add(s.shackle, { y: -10, duration: 260, ease: 'out(3)' })
        .add(s.shackle, { rotate: 30, duration: 900, ease: 'outElastic(1, .45)' }, '-=120')
        .add(s.body, { scaleY: [1, 0.9, 1], scaleX: [1, 1.05, 1], duration: 420, ease: 'out(2)' }, '<<')
        .add(s.rays, { opacity: [0, 1, 0], scale: [0.4, 1.2], duration: 520, ease: 'out(2)', delay: anime.stagger(50) }, '<<')
        .call(function () { if (s.color) s.caseEl.style.fill = s.color; }, '<<');
    }

    function lock(s) {
      s.open = false;
      label(s);
      s.el.dispatchEvent(new CustomEvent('toggle', { bubbles: true, detail: { open: false } }));
      if (s.color) s.caseEl.style.fill = '';
      if (reduced()) { place(s); return; }
      var tl = anime.createTimeline();
      tl.add(s.shackle, { rotate: 0, duration: 380, ease: 'inOut(3)' })
        .add(s.shackle, { y: 0, duration: 200, ease: 'in(3)' })
        // A thud as it clicks home.
        .add(s.body, { y: [0, 3, 0], duration: 320, ease: 'out(2)' }, '-=20')
        .add(s.key, { scale: [1, 0.8, 1], duration: 300, ease: 'inOut(2)' }, '-=200');
    }

    function toggle(s) { if (s.open) lock(s); else unlock(s); }

    function start(s) {
      var d = s.el.dataset;
      var hold = +d.hold || DEFAULTS.hold;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var locked = d.locked !== undefined && d.locked !== 'false';
      var cycle = function () {
        if (s.stopped) return;
        toggle(s);
        if (loop) s.timer = setTimeout(cycle, hold + (s.open ? 1300 : 600));
      };
      if (!locked) s.timer = setTimeout(cycle, +d.delay || 300);
    }

    function setup(el) {
      el.classList.add('pdl');
      var s = build(el);
      el._pdl = s;
      label(s);
      place(s);
      // In a decorative preview (aria-hidden), it just plays: no tapping.
      if (el.closest('[aria-hidden="true"]')) return;
      if (!el.getAttribute('role') && el.tagName !== 'BUTTON') {
        el.setAttribute('role', 'button');
        el.tabIndex = 0;
      }
      var tap = function () {
        s.stopped = true;
        clearTimeout(s.timer);
        toggle(s);
      };
      el.addEventListener('click', tap);
      el.addEventListener('keydown', function (e) {
        if (el.tagName === 'BUTTON') return;
        if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); tap(); }
      });
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          start(e.target._pdl);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-padlock-unlock]').forEach(function (el) {
        if (el._pdl) return;
        setup(el);
        if (reduced()) {
          if (el.dataset.locked === undefined || el.dataset.locked === 'false') { el._pdl.open = true; label(el._pdl); place(el._pdl); }
        } else io.observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.padlock = function (el) { if (el._pdl) toggle(el._pdl); };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<span data-padlock-unlock style="font-size: 96px; color: #7c3aed"></span>

<!-- locked until tapped, turning green when open: -->
<button data-padlock-unlock data-locked data-open-color="#16a34a"></button>