Edit-mode Jiggle
The element jiggles on the spot like an app icon waiting to be rearranged. Each one gets its own slightly different rhythm, so a whole grid looks natural rather than marching in step.
How to use it
- Paste the snippet just before
</head>. - Add
data-edit-mode-jiggleto each element. They jiggle while they’re on screen. - Or add
data-presstoo, to start them still: a long press on one starts all of them in the same parent, and a tap anywhere else stops them.
Options
Add any of these attributes to the same element.
data-angle | How far it tilts each way, in degrees. Default 1.6. |
data-speed | How long one jiggle takes. Default 260 ms. |
data-badge | Show a round “remove” badge on the top-left corner while it jiggles. |
data-press | Start still, and jiggle after a long press, as above. |
Call animacious.jiggle(el) to start it from your own code and animacious.jiggle(el, false) to stop it, for example from an Edit / Done button. With reduced motion it stays still but still shows its badge.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Edit-mode Jiggle: the element jiggles on the spot, like an app icon
// waiting to be rearranged. Each element gets its own slightly different
// rhythm, so a grid of them looks natural. It pauses while off screen.
// Usage: <span class="icon" data-edit-mode-jiggle>…</span>
// Optional: data-angle (degrees each way, default 1.6), data-speed (ms per
// jiggle, default 260), data-badge (show a round "remove" badge on the
// top-left corner while it jiggles), data-press (start still: a long press
// starts every jiggler in the same parent, a tap outside them stops them).
// Script use: animacious.jiggle(el) starts it, animacious.jiggle(el, false)
// stops it.
var DEFAULTS = { angle: 1.6, speed: 260 };
var css =
'@keyframes emj-a{0%,100%{transform:rotate(calc(var(--emj-a) * -1))}50%{transform:rotate(var(--emj-a))}}' +
'@keyframes emj-b{0%,100%{transform:rotate(var(--emj-a)) translateY(0)}25%{transform:rotate(0deg) translateY(-.6px)}' +
'50%{transform:rotate(calc(var(--emj-a) * -1)) translateY(0)}75%{transform:rotate(0deg) translateY(.6px)}}' +
'.emj-on{animation:emj-a var(--emj-s) ease-in-out infinite;animation-delay:var(--emj-d)}' +
'.emj-on.emj-alt{animation-name:emj-b}' +
'.emj-paused{animation-play-state:paused}' +
'.emj-badge{position:absolute;left:-7px;top:-7px;z-index:2;display:grid;place-items:center;width:22px;height:22px;' +
'border-radius:50%;background:rgba(60,60,67,.82);box-shadow:0 1px 3px rgba(0,0,0,.25);pointer-events:none;' +
'transform:scale(0);transition:transform .25s cubic-bezier(.3,1.6,.5,1)}' +
'.emj-badge::before{content:"";width:10px;height:2px;border-radius:1px;background:#fff}' +
'.emj-on>.emj-badge,.emj-still>.emj-badge{transform:scale(1)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function on(v) { return v !== undefined && v !== 'false'; }
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
var count = 0;
function setup(el) {
var d = el.dataset;
var speed = +d.speed || DEFAULTS.speed;
var angle = d.angle === undefined ? DEFAULTS.angle : +d.angle;
// Every element gets a slightly different speed, start point and pattern.
el.style.setProperty('--emj-a', angle + 'deg');
el.style.setProperty('--emj-s', Math.round(speed * (0.9 + Math.random() * 0.2)) + 'ms');
el.style.setProperty('--emj-d', -Math.round(Math.random() * speed) + 'ms');
if (count++ % 2) el.classList.add('emj-alt');
if (on(d.badge)) {
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
var b = document.createElement('span');
b.className = 'emj-badge';
b.setAttribute('aria-hidden', 'true');
el.appendChild(b);
}
el._emj = { active: false };
}
function jiggle(el, yes) {
var s = el._emj;
if (!s) return;
s.active = yes !== false;
// With reduced motion it stays still, but still shows the badge.
el.classList.toggle(reduced() ? 'emj-still' : 'emj-on', s.active);
}
function siblings(el) {
return Array.prototype.filter.call(el.parentElement.children, function (c) { return c._emj; });
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
e.target.classList.toggle('emj-paused', !e.isIntersecting);
});
}, { threshold: 0 });
var pressable = [];
document.querySelectorAll('[data-edit-mode-jiggle]').forEach(function (el) {
if (el._emj) return;
setup(el);
io.observe(el);
if (!on(el.dataset.press)) { jiggle(el); return; }
pressable.push(el);
var timer;
el.addEventListener('pointerdown', function () {
clearTimeout(timer);
timer = setTimeout(function () { siblings(el).forEach(function (c) { jiggle(c); }); }, 500);
});
['pointerup', 'pointerleave', 'pointercancel'].forEach(function (t) {
el.addEventListener(t, function () { clearTimeout(timer); });
});
el.addEventListener('contextmenu', function (e) { e.preventDefault(); });
});
if (pressable.length && !init.bound) {
init.bound = true;
// A tap anywhere outside the jigglers ends edit mode.
document.addEventListener('pointerdown', function (e) {
if (e.target.closest && e.target.closest('[data-edit-mode-jiggle]')) return;
document.querySelectorAll('[data-edit-mode-jiggle][data-press]').forEach(function (el) { jiggle(el, false); });
});
}
}
window.animacious = window.animacious || {};
window.animacious.jiggle = jiggle;
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div class="home-screen">
<span class="app" data-edit-mode-jiggle data-badge>…</span>
<span class="app" data-edit-mode-jiggle data-badge>…</span>
</div>
<!-- or only after a long press: -->
<span class="app" data-edit-mode-jiggle data-badge data-press>…</span>