Clip-path Reveal
An image is uncovered by a shape that grows as you scroll, a circle opening from the middle by default, until the whole picture shows. Tied to the scroll, so it closes again if you scroll back. Try a diamond, a rounded rectangle or a wipe.
Scroll inside ↓
data-shape="circle"
data-shape="diamond"
data-shape="inset"
How to use it
- Paste the snippet just before
</head>. - Add
data-clip-path-revealto an image, video or any element.
Options
Add any of these attributes to the same element.
data-shape | circle (default), inset (a rounded rectangle growing from the middle), diamond or wipe (rising from the bottom). |
data-origin | Where a circle opens from, e.g. 0% 100% for the bottom-left corner. Default the middle. |
data-start | Where the element’s top is in the view when the reveal starts, from 0 (top) to 1 (bottom). Default 0.95. |
data-end | Where its top is when the reveal finishes. Default 0.25. |
Works inside scrolling boxes too.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Clip-path Reveal: an image is uncovered by a shape that grows as you
// scroll, a circle opening from the middle by default, until the whole
// picture shows. Tied to the scroll, so it closes again if you scroll back.
// Usage: <img data-clip-path-reveal src="photo.jpg" alt="…">
// or: <div data-clip-path-reveal data-shape="diamond">…</div>
// Optional: data-shape ("circle" (default), "inset" (a rounded rectangle
// growing from the middle), "diamond", or "wipe" (rising from the bottom)),
// data-origin (where a circle opens from, default "50% 50%"), data-start and
// data-end (where the element's top is in the view, 0 top to 1 bottom, as the
// reveal starts and finishes: default 0.95 and 0.25).
// Works inside scrolling boxes too: it follows the nearest scrolling parent.
var DEFAULTS = { start: 0.95, end: 0.25, origin: '50% 50%' };
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') && p.scrollHeight > p.clientHeight) return p;
}
return null;
}
function num(v, fallback) { return v !== undefined && v !== '' && !isNaN(+v) ? +v : fallback; }
// The clip for each shape, from t = 0 (nothing showing) to 1 (all of it).
var SHAPES = {
// A radius of 71% reaches the corners from the middle; from anywhere else
// it may need up to 142%.
circle: function (t, o) { return 'circle(' + (t * (o === DEFAULTS.origin ? 71 : 142)).toFixed(2) + '% at ' + o + ')'; },
inset: function (t) {
var i = (1 - t) * 50, r = 24 * (1 - t) + 4;
return 'inset(' + i.toFixed(2) + '% ' + i.toFixed(2) + '% round ' + r.toFixed(1) + 'px)';
},
diamond: function (t) {
var a = t * 100;
return 'polygon(50% ' + (50 - a).toFixed(2) + '%, ' + (50 + a).toFixed(2) + '% 50%, 50% ' + (50 + a).toFixed(2) + '%, ' + (50 - a).toFixed(2) + '% 50%)';
},
wipe: function (t) {
// A gently curved edge that flattens as it reaches the top.
var y = (1 - t) * 100;
return 'inset(' + y.toFixed(2) + '% 0 0 0 round ' + (40 * (1 - t)).toFixed(1) + '% ' + (40 * (1 - t)).toFixed(1) + '% 0 0)';
}
};
function setup(el) {
var d = el.dataset;
var shape = SHAPES[d.shape] || SHAPES.circle;
var origin = d.origin || DEFAULTS.origin;
var start = num(d.start, DEFAULTS.start), end = num(d.end, DEFAULTS.end);
var box = scroller(el);
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;
// Ease in and out, so the shape opens smoothly and settles fully open.
var t = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
el.style.clipPath = t >= 1 ? 'none' : shape(t, origin);
}
function onScroll() {
if (queued) return;
queued = true;
requestAnimationFrame(update);
}
(box || window).addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
update();
}
function init() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
document.querySelectorAll('[data-clip-path-reveal]').forEach(function (el) {
if (el._cpr) return;
el._cpr = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><img src="aurora.jpg" alt="Northern lights" data-clip-path-reveal>
<!-- a diamond: -->
<img src="aurora.jpg" alt="Northern lights" data-clip-path-reveal data-shape="diamond">