← Scroll

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 ↓

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-laptop-open-on-scroll to 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-startWhere the laptop’s top is in the view as the lid starts lifting, from 0 (top) to 1 (bottom). Default 0.9.
data-endWhere its top is once it’s fully open. Default 0.25.
data-angleHow far the lid opens, in degrees. Default 100.
data-colorThe 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.

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