Vinyl Sleeve Slide
An album cover with a record tucked inside. Hover or tap and the record slides out of the sleeve, rolling as it goes, then spins up to speed with the cover art on its label. Move away and it slows down and slides back in.
Bloom
Hover or tap the sleeve
How to use it
- Paste the snippet just before
</head>. - Add
data-vinyl-sleeve-slideto an element holding the cover art (an<img>or anything else), and give it a width. The record is drawn for you.
Options
Add any of these attributes to the same element.
data-trigger | hover (also toggles on tap), click, or view to slide out once when it scrolls into view. Default hover. |
data-autoplay | Milliseconds: slide out and back in on a timer until someone interacts. |
data-out | How far the record comes out, as a share of the sleeve. Default 0.55. |
data-rpm | How fast it spins. Default 33. |
data-label | A colour for the record’s label instead of the cover art. |
data-center="false" | Don’t shift the sleeve left to keep the pair centred. |
The label is a small copy of the cover, so you can see it turning. The spin only runs while it’s on screen, and with reduced motion the record simply sits half out. From code: animacious.vinyl(element, true).
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Vinyl Sleeve Slide: an album cover with a record tucked inside. Hover,
// tap or scroll it into view and the record slides out of the sleeve,
// rolling as it goes, then spins up to speed with the cover art on its
// label. Leave and it slows down and slides back in.
// Usage: <div data-vinyl-sleeve-slide style="width: 240px"><img src="cover.jpg" alt="Album name"></div>
// The child is the cover art; the record is drawn for you.
// Optional: data-trigger ("hover", default, also toggles on tap; "click";
// or "view": slides out once when scrolled into view), data-autoplay (ms
// between sliding out and back in, until someone interacts), data-out (how
// far the record comes out, as a share of the sleeve, default 0.55),
// data-rpm (spin speed, default 33), data-label (a colour for the label
// instead of the cover art), data-center="false" (don't shift the sleeve
// to keep the pair centred).
// Script use: animacious.vinyl(el, true|false).
var DEFAULTS = { out: 0.55, rpm: 33 };
var css =
':where(.vss){aspect-ratio:1}' +
'.vss{position:relative;-webkit-tap-highlight-color:transparent}' +
'.vss-wrap{position:absolute;inset:0}' +
'.vss-sleeve{position:absolute;inset:0;z-index:2;overflow:hidden;border-radius:3px;box-shadow:0 1px 2px rgba(0,0,0,.2),0 12px 28px rgba(15,15,30,.28)}' +
'.vss-sleeve>*{display:block;width:100%;height:100%;object-fit:cover}' +
'.vss-sleeve::after{content:"";position:absolute;inset:0;background:linear-gradient(115deg,rgba(255,255,255,.18),transparent 40%,rgba(0,0,0,.12));pointer-events:none}' +
'.vss-disc{position:absolute;top:2.5%;left:2.5%;width:95%;height:95%;z-index:1;border-radius:50%;box-shadow:0 8px 22px rgba(15,15,30,.35)}' +
'.vss-spin{position:absolute;inset:0;border-radius:50%;background:radial-gradient(circle,#0000 0 30%,#2a2a2e 30.5% 31.5%,#0000 32% 67%,#2a2a2e 67.5% 68%,#0000 68.5%),repeating-radial-gradient(circle,#141416 0 1.5px,#1f1f23 1.5px 3px)}' +
'.vss-label{position:absolute;inset:33%;border-radius:50%;overflow:hidden;background:#e11d48}' +
'.vss-label>*{display:block;width:100%!important;height:100%!important;object-fit:cover}' +
'.vss-label::after{content:"";position:absolute;left:50%;top:50%;width:9%;height:9%;margin:-4.5% 0 0 -4.5%;border-radius:50%;background:#f4f4f5;box-shadow:inset 0 1px 2px rgba(0,0,0,.5)}' +
'.vss-sheen{position:absolute;inset:0;border-radius:50%;pointer-events:none;background:conic-gradient(from 20deg,#0000 0 6%,rgba(255,255,255,.13) 11%,#0000 17% 50%,rgba(255,255,255,.1) 61%,#0000 67%)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function stripIds(node) {
if (node.removeAttribute) {
node.removeAttribute('id');
node.querySelectorAll('[id]').forEach(function (n) { n.removeAttribute('id'); });
}
return node;
}
function setup(el) {
var d = el.dataset;
var art = Array.prototype.slice.call(el.children);
el.classList.add('vss');
var wrap = document.createElement('div');
wrap.className = 'vss-wrap';
var sleeve = document.createElement('div');
sleeve.className = 'vss-sleeve';
art.forEach(function (n) { sleeve.appendChild(n); });
var disc = document.createElement('div');
disc.className = 'vss-disc';
disc.setAttribute('aria-hidden', 'true');
var spin = document.createElement('div');
spin.className = 'vss-spin';
var label = document.createElement('div');
label.className = 'vss-label';
if (d.label) label.style.background = d.label;
else if (art[0]) {
var copy = stripIds(art[0].cloneNode(true));
copy.removeAttribute('alt');
copy.setAttribute('aria-hidden', 'true');
label.appendChild(copy);
}
spin.appendChild(label);
var sheen = document.createElement('div');
sheen.className = 'vss-sheen';
disc.appendChild(spin);
disc.appendChild(sheen);
wrap.appendChild(disc);
wrap.appendChild(sleeve);
el.appendChild(wrap);
var s = el._vssS = { wrap: wrap, disc: disc, spin: spin, v: { p: 0 }, open: false, anim: null, angle: 0, w: 0, raf: 0, last: 0, visible: false, timer: 0 };
var thumb = !!el.closest('[aria-hidden="true"]');
var trigger = d.trigger || 'hover';
if (reduced()) { s.v.p = 1; s.open = true; apply(el); return; }
if (trigger === 'hover') {
el.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') { stop(el); toggle(el, true); } });
el.addEventListener('pointerleave', function (e) { if (e.pointerType === 'mouse') toggle(el, false); });
// A tap also focuses it, so keyboard focus only counts when it isn't a press.
var pressing = false;
el.addEventListener('pointerdown', function () { pressing = true; });
el.addEventListener('pointerup', function (e) {
pressing = false;
if (e.pointerType !== 'mouse') { stop(el); toggle(el, !s.open); }
});
el.addEventListener('focusin', function () { if (!pressing) toggle(el, true); });
el.addEventListener('focusout', function () { if (!pressing) toggle(el, false); });
if (!thumb && !el.hasAttribute('tabindex')) el.tabIndex = 0;
} else if (trigger === 'click') {
el.style.cursor = 'pointer';
if (!thumb) {
el.setAttribute('role', 'button');
el.setAttribute('aria-pressed', 'false');
if (!el.hasAttribute('tabindex')) el.tabIndex = 0;
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Play record');
}
el.addEventListener('click', function () { stop(el); toggle(el, !s.open); });
el.addEventListener('keydown', function (e) {
if (e.key !== 'Enter' && e.key !== ' ') return;
e.preventDefault();
stop(el);
toggle(el, !s.open);
});
}
new IntersectionObserver(function (en) {
s.visible = en[0].isIntersecting;
if (s.visible) {
if (trigger === 'view' && !s.seen) { s.seen = true; setTimeout(function () { toggle(el, true); }, 300); }
kick(el);
}
}, { threshold: 0.3 }).observe(el);
if (d.autoplay !== undefined && d.autoplay !== 'false') {
s.timer = setInterval(function () { if (s.visible) toggle(el, !s.open); }, +d.autoplay || 3200);
setTimeout(function () { if (s.timer && s.visible && !s.open) toggle(el, true); }, 600);
}
}
function apply(el) {
var s = el._vssS, d = el.dataset;
var out = d.out !== undefined ? +d.out : DEFAULTS.out;
var p = s.v.p;
// The disc is 95% of the sleeve, so its own percentages need scaling.
s.disc.style.transform = 'translateX(' + (p * out / 0.95 * 100).toFixed(2) + '%)';
if (d.center !== 'false') s.wrap.style.transform = 'translateX(' + (-p * out * 50).toFixed(2) + '%)';
// It rolls a little as it slides, then the spin takes over.
s.spin.style.transform = 'rotate(' + (s.angle + p * 160).toFixed(1) + 'deg)';
}
function kick(el) {
var s = el._vssS;
if (s.raf || !s.visible) return;
s.last = performance.now();
s.raf = requestAnimationFrame(function tick(t) {
s.raf = 0;
var dt = Math.min(50, t - s.last);
s.last = t;
var rpm = +el.dataset.rpm || DEFAULTS.rpm;
var target = s.open && s.v.p > 0.97 ? rpm * 6 / 1000 : 0; // degrees per ms
// Spin up gently like a turntable, and slow down a touch quicker.
s.w += (target - s.w) * (target > s.w ? 0.035 : 0.08);
if (Math.abs(target - s.w) < 0.0005) s.w = target;
s.angle = (s.angle + s.w * dt) % 360;
apply(el);
if (s.visible && (s.w > 0 || target > 0 || s.sliding)) s.raf = requestAnimationFrame(tick);
});
}
function stop(el) {
var s = el._vssS;
if (s && s.timer) { clearInterval(s.timer); s.timer = 0; }
}
function toggle(el, open) {
var s = el._vssS;
if (!s || s.open === open) return;
s.open = open;
if (el.getAttribute('role') === 'button') el.setAttribute('aria-pressed', open);
if (s.anim) s.anim.pause();
if (reduced()) { s.v.p = open ? 1 : 0; apply(el); return; }
s.sliding = true;
s.anim = anime.animate(s.v, {
p: open ? 1 : 0,
duration: open ? 1000 : 850,
delay: open ? 0 : 250,
ease: open ? 'out(3)' : 'inOut(3)',
onUpdate: function () { if (!s.raf) apply(el); },
onComplete: function () { s.sliding = false; kick(el); }
});
kick(el);
}
function init() {
document.querySelectorAll('[data-vinyl-sleeve-slide]').forEach(function (el) {
if (el._vss) return;
el._vss = true;
setup(el);
});
}
window.animacious = window.animacious || {};
window.animacious.vinyl = function (el, open) { stop(el); toggle(el, open === undefined ? !(el._vssS && el._vssS.open) : !!open); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-vinyl-sleeve-slide style="width: 240px">
<img src="cover.jpg" alt="Bloom by The Petals">
</div>