Section Uncover
The section you’re reading lifts away like a sheet of paper, and the next one is uncovered from beneath it, already waiting in place. It starts a little sunk and shaded and rises to meet you as the sheet clears it.
Scroll inside ↓
How to use it
- Paste the snippet just before
</head>. - Add
data-section-uncoverto the section to be uncovered. The section just before it is the sheet that lifts away, so give it a background. - Keep both sections in the same parent. The uncovered one works best no taller than the screen.
Options
Add any of these attributes to the same element.
data-depth | How far its content starts sunk, as a scale. Default 0.92; 1 for none. |
data-shade | How dark it starts, from 0 to 1. Default 0.45; 0 for none. |
data-shadow | The shadow under the lifting sheet, as a CSS box-shadow, or none. |
It pins with position: sticky, so nothing around it can have overflow: hidden. The rise is a transform on the section’s children, so don’t give them one of your own. With reduced motion it is still uncovered, just without rising. Works inside scrolling boxes too.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Section Uncover: the section before this one scrolls away like a sheet
// lifting off, and this section is uncovered from beneath it, waiting in
// place. It starts a little sunk and shaded, and rises to full brightness
// as the sheet clears it.
// Usage: <section class="hero">…</section>
// <section data-section-uncover>…</section>
// The section before it needs a background (it is the sheet that slides
// away), and both need to sit in the same parent. The uncovered section
// works best no taller than the screen.
// Optional: data-depth (how far the content starts sunk, as a scale,
// default 0.92; 1 for none), data-shade (how dark it starts, 0 to 1,
// default 0.45; 0 for none), data-shadow (the shadow under the lifting
// sheet, any CSS box-shadow, or "none").
// The sinking and rising is a transform on the section's children, so
// don't put a transform of your own on them.
// Works inside scrolling boxes too: it follows the nearest scrolling parent.
var DEFAULTS = { depth: 0.92, shade: 0.45, shadow: '0 18px 40px -8px rgba(15,10,40,.45)' };
var css =
'.secu{position:sticky!important;bottom:0;z-index:0;overflow:hidden}' +
'.secu>:not(.secu-shade){transform:translate3d(0,var(--secu-y,0),0) scale(var(--secu-s,1))}' +
'.secu-shade{position:absolute;inset:0;pointer-events:none;background:#0b0820}' +
'.secu-sheet{position:relative;z-index:1}';
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 ease(t) { return 1 - Math.pow(1 - t, 3); }
function setup(el) {
var d = el.dataset;
var depth = Math.max(0.5, Math.min(1, num(d.depth, DEFAULTS.depth)));
var shadeMax = Math.max(0, Math.min(1, num(d.shade, DEFAULTS.shade)));
var sheet = el.previousElementSibling;
var box = scroller(el);
el.classList.add('secu');
if (sheet) {
sheet.classList.add('secu-sheet');
if (d.shadow !== 'none') sheet.style.boxShadow = d.shadow || DEFAULTS.shadow;
}
if (reduced()) return; // still uncovered from beneath, just without the rising and shading
var shade = document.createElement('div');
shade.className = 'secu-shade';
shade.setAttribute('aria-hidden', 'true');
el.appendChild(shade);
var queued = false, last = -1;
function update() {
queued = false;
var viewTop = box ? box.getBoundingClientRect().top + box.clientTop : 0;
var viewH = box ? box.clientHeight : window.innerHeight;
var h = el.offsetHeight || 1;
// How much of the section the sheet has cleared so far.
var shown = sheet ? viewTop + viewH - sheet.getBoundingClientRect().bottom : h;
var p = Math.max(0, Math.min(1, shown / Math.min(h, viewH)));
if (Math.abs(p - last) < 0.001) return;
last = p;
var e = ease(p);
el.style.setProperty('--secu-y', ((1 - e) * 24).toFixed(2) + 'px');
el.style.setProperty('--secu-s', (depth + (1 - depth) * e).toFixed(4));
shade.style.opacity = (shadeMax * (1 - e)).toFixed(3);
}
function onScroll() {
if (queued) return;
queued = true;
requestAnimationFrame(update);
}
update();
(box || window).addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', function () { last = -1; onScroll(); });
}
function init() {
document.querySelectorAll('[data-section-uncover]').forEach(function (el) {
if (el._secu) return;
el._secu = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section class="hero">…</section>
<section data-section-uncover>
<h2>Underneath</h2>
</section>