Laptop Open on Scroll
A laptop, built in CSS around your screenshot, starts closed and opens as it scrolls into view: the lid lifts, the screen lights up, and the whole laptop turns to face you and comes forward. Scroll back and it closes again.
Scroll inside ↓
2.4k68%$9.1k
How to use it
- Paste the snippet just before
</head>. - Add
data-laptop-open-on-scrollto an element holding your screenshot (an image, video or any HTML) and give it a width. Its contents become the 16:10 screen.
Options
Add any of these attributes to the same element.
data-start | Where the laptop’s top is in the view as the lid starts lifting, from 0 (top) to 1 (bottom). Default 0.9. |
data-end | Where its top is once it’s fully open. Default 0.25. |
data-angle | How far the lid opens, in degrees. Default 100. |
data-color | The case colour. Default #d4d4dc (silver). |
It adds some room below for the keyboard. With reduced motion it’s simply open. Works inside scrolling boxes too.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Laptop Open on Scroll: a laptop, built from CSS around your screenshot,
// starts closed and opens as it scrolls into view: the lid lifts, the
// screen lights up and the whole laptop turns towards you and comes
// forward. Tied to the scroll, so it closes again if you scroll back.
// Usage: <div data-laptop-open-on-scroll>
// <img src="screenshot.png" alt="…">
// </div>
// Whatever is inside becomes the screen (16:10). Give the element a width,
// e.g. max-width: 640px; it adds room below for the keyboard.
// Optional: data-start and data-end (where the laptop's top is in the view,
// 0 top to 1 bottom, as it starts and finishes opening; default 0.85 and
// 0.2), data-angle (how far the lid opens, in degrees, default 100),
// data-color (the case colour, default #d4d4dc).
// Works inside scrolling boxes too: it follows the nearest scrolling parent.
var DEFAULTS = { start: 0.85, end: 0.2, angle: 100, color: '#d4d4dc' };
var css =
'.los{position:relative;perspective:1800px;perspective-origin:50% 0%;padding-bottom:12%;--los-case:#d4d4dc}' +
'.los-device{position:relative;width:100%;transform-style:preserve-3d;transform-origin:50% 100%}' +
'.los-lid{position:relative;width:86%;margin:0 auto;aspect-ratio:16/10.6;transform-style:preserve-3d;transform-origin:50% 100%}' +
'.los-screen,.los-back{position:absolute;inset:0;border-radius:3.2% 3.2% 0 0/4.8% 4.8% 0 0;-webkit-backface-visibility:hidden;backface-visibility:hidden}' +
'.los-screen{background:#0b0b12;padding:2.6% 2.6% 4.2%;box-shadow:inset 0 0 0 1.5px #3a3a44}' +
'.los-display{position:relative;width:100%;height:100%;overflow:hidden;border-radius:2px;background:#fff}' +
'.los-display>img,.los-display>video{display:block;width:100%;height:100%;object-fit:cover;object-position:top}' +
'.los-dark{position:absolute;inset:0;background:#05050a;pointer-events:none}' +
'.los-glare{position:absolute;inset:0;background:linear-gradient(120deg,rgba(255,255,255,.16),rgba(255,255,255,0) 40%);pointer-events:none}' +
'.los-back{transform:rotateY(180deg);background:linear-gradient(160deg,color-mix(in srgb,var(--los-case),#fff 35%),var(--los-case) 60%,color-mix(in srgb,var(--los-case),#000 12%));box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}' +
'.los-back::after{content:"";position:absolute;left:50%;top:50%;width:9%;aspect-ratio:1;border-radius:50%;transform:translate(-50%,-50%);background:rgba(255,255,255,.55);box-shadow:0 0 0 1px rgba(0,0,0,.06)}' +
'.los-base{position:absolute;left:0;right:0;top:100%;aspect-ratio:16/9;transform-origin:50% 0;transform:rotateX(90deg);transform-style:preserve-3d;border-radius:0 0 4% 4%/0 0 9% 9%;' +
'background:linear-gradient(180deg,color-mix(in srgb,var(--los-case),#000 8%),var(--los-case) 12%,color-mix(in srgb,var(--los-case),#fff 25%))}' +
'.los-keys{position:absolute;left:12%;right:12%;top:12%;height:44%;border-radius:4px;background-color:color-mix(in srgb,var(--los-case),#000 55%);' +
'background-image:linear-gradient(90deg,transparent 88%,color-mix(in srgb,var(--los-case),#000 35%) 0),linear-gradient(0deg,transparent 80%,color-mix(in srgb,var(--los-case),#000 35%) 0);background-size:6.66% 100%,100% 16.66%}' +
'.los-pad{position:absolute;left:36%;width:28%;top:62%;height:30%;border-radius:6px;background:color-mix(in srgb,var(--los-case),#000 7%);box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}' +
'.los-edge{position:absolute;left:0;right:0;top:100%;height:6px;transform-origin:50% 0;transform:rotateX(-90deg);border-radius:0 0 8px 8px;background:color-mix(in srgb,var(--los-case),#000 22%)}' +
'.los-shadow{position:absolute;left:4%;right:4%;bottom:4%;height:10%;border-radius:50%;background:radial-gradient(closest-side,rgba(15,15,30,.28),rgba(15,15,30,0));pointer-events:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function num(v, fallback) { return v !== undefined && v !== '' && !isNaN(+v) ? +v : fallback; }
function scroller(el) {
for (var p = el.parentElement; p && p !== document.body; p = p.parentElement) {
var o = getComputedStyle(p).overflowY;
if (o === 'auto' || o === 'scroll') return p;
}
return null;
}
function div(cls, parent) {
var n = document.createElement('div');
n.className = cls;
if (parent) parent.appendChild(n);
return n;
}
function setup(el) {
var d = el.dataset;
var start = num(d.start, DEFAULTS.start), end = num(d.end, DEFAULTS.end);
var angle = num(d.angle, DEFAULTS.angle);
var box = scroller(el);
// Build the laptop around the original content.
var kids = Array.prototype.slice.call(el.childNodes);
el.classList.add('los');
if (d.color) el.style.setProperty('--los-case', d.color);
var shadow = div('los-shadow', el);
shadow.setAttribute('aria-hidden', 'true');
var device = div('los-device', el);
var lid = div('los-lid', device);
var screen = div('los-screen', lid);
var display = div('los-display', screen);
kids.forEach(function (k) { display.appendChild(k); });
var dark = div('los-dark', display);
div('los-glare', display);
div('los-back', lid);
var base = div('los-base', device);
base.setAttribute('aria-hidden', 'true');
div('los-keys', base);
div('los-pad', base);
div('los-edge', base);
function render(p) {
// The lid lifts first and fastest; the laptop turns to face you and
// comes forward over the whole scroll.
var lp = Math.min(1, p / 0.85);
var le = 1 - Math.pow(1 - lp, 3);
var e = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
lid.style.transform = 'rotateX(' + (-88 + (angle - 2) * le).toFixed(2) + 'deg)';
device.style.transform = 'translateY(' + (-30 * (1 - e)).toFixed(2) + '%) rotateX(' + (-34 + 26 * e).toFixed(2) + 'deg) scale(' + (0.8 + 0.2 * e).toFixed(4) + ')';
dark.style.opacity = Math.max(0, 1 - Math.max(0, lp - 0.35) / 0.5).toFixed(3);
shadow.style.opacity = (0.6 + 0.4 * e).toFixed(3);
}
if (reduced()) { render(1); return; }
var queued = false, last = -1;
function update() {
queued = false;
var r = el.getBoundingClientRect();
var top = 0, h = window.innerHeight;
if (box) { var b = box.getBoundingClientRect(); top = b.top + box.clientTop; h = box.clientHeight; }
var p = (top + h * start - r.top) / (h * (start - end) || 1);
p = Math.max(0, Math.min(1, p));
if (Math.abs(p - last) < 0.0005) return;
last = p;
render(p);
}
function onScroll() {
if (queued) return;
queued = true;
requestAnimationFrame(update);
}
(box || window).addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
update();
}
function init() {
document.querySelectorAll('[data-laptop-open-on-scroll]').forEach(function (el) {
if (el._los) return;
el._los = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-laptop-open-on-scroll style="max-width: 640px; margin: auto">
<img src="app-screenshot.png" alt="The app's dashboard">
</div>
<!-- space grey: -->
<div data-laptop-open-on-scroll data-color="#6b6b75">…</div>