Scroll Tilt-to-flat
A screenshot starts tilted back in 3D, leaning away like a screen on a desk, and swings up to lie flat and full size as you scroll down to it. Scroll back up and it leans away again. A favourite for product hero sections.
Scroll inside ↓
Meet your dashboardEverything you need, in one place.
2.4k68%$9.1k
How to use it
- Paste the snippet just before
</head>. - Add
data-scroll-tilt-to-flatto a screenshot, a framed image, a video or any element.
Options
Add any of these attributes to the same element.
data-tilt | How far it leans back to start with, in degrees. Default 48. |
data-scale | Its starting size. Default 0.86. |
data-start | Where its top is in the view as it starts to swing up, from 0 (top) to 1 (bottom). Default 0.95. |
data-end | Where its top is once it lies flat. Default 0.3. |
data-perspective | Depth of the 3D, in px. Default 1.2 times its width; smaller looks more dramatic. |
Works inside scrolling boxes too.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Scroll Tilt-to-flat: a screenshot starts tilted back in 3D, leaning away
// from you like a screen on a desk, and lies flat (facing you, full size)
// as you scroll down to it. Tied to the scroll, so it tilts back again if
// you scroll up.
// Usage: <img data-scroll-tilt-to-flat src="dashboard.png" alt="…">
// (or on any element: a framed screenshot, a video, a card).
// Optional: data-tilt (starting tilt in degrees, default 48), data-scale
// (starting size, default 0.86), data-start and data-end (where the
// element's top is in the view, 0 top to 1 bottom, as it starts and
// finishes lying flat; default 0.95 and 0.3), data-perspective (in px,
// default 1.2 times its width: smaller looks more dramatic).
// Works inside scrolling boxes too: it follows the nearest scrolling parent.
var DEFAULTS = { tilt: 48, scale: 0.86, start: 0.95, end: 0.3 };
var css = '.stf{transform-origin:50% 60%}';
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 setup(el) {
var d = el.dataset;
var tilt = num(d.tilt, DEFAULTS.tilt), scale = num(d.scale, DEFAULTS.scale);
var start = num(d.start, DEFAULTS.start), end = num(d.end, DEFAULTS.end);
var persp = num(d.perspective, 0);
var box = scroller(el);
var queued = false, last = -1;
el.classList.add('stf');
function update() {
queued = false;
// Measure without the tilt, so it doesn't feed back into the progress.
var saved = el.style.transform;
el.style.transform = 'none';
var r = el.getBoundingClientRect();
el.style.transform = saved;
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;
var e = p * p * (3 - 2 * p);
var a = tilt * (1 - e), s = scale + (1 - scale) * e;
el.style.transform = e >= 1 ? 'none'
: 'perspective(' + Math.round(persp || Math.max(400, el.offsetWidth * 1.2)) + 'px) rotateX(' + a.toFixed(2) + 'deg) scale(' + s.toFixed(4) + ')';
}
function onScroll() {
if (queued) return;
queued = true;
requestAnimationFrame(update);
}
(box || window).addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
update();
}
function init() {
if (reduced()) return;
document.querySelectorAll('[data-scroll-tilt-to-flat]').forEach(function (el) {
if (el._stf) return;
el._stf = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><img data-scroll-tilt-to-flat src="dashboard.png" alt="The dashboard">
<!-- a steeper lean: -->
<div class="frame" data-scroll-tilt-to-flat data-tilt="45">…</div>