Clip reveal
The image reveal of a studio site, held to its curve.
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialThe frame owes itself a size: the picture fills the frame, so give the box a ratio or a height, and the imagewidth: 100%,height: 100%andobject-fit: cover. Aborder-radiuson the frame is carried inside the clip while it opens. Before the script runs, a marked frame is hidden withvisibilityand keeps its room; it shows by itself after four seconds if the script never comes, and at once without JavaScript, through the noscript line the paste carries inside its Embed. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Use it on your own elements
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the element on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-clip-reveal="up" the frame opens from its bottom edge upwards, and this is what an empty value does data-clip-reveal="down" it opens from its top edge downwards data-clip-reveal="left" it opens from its right edge leftwards, the rule standing upright data-clip-reveal="right" it opens from its left edge rightwards data-clip-reveal="center" it opens outwards from its middle, and the rule is the outline of the window data-clip-reveal-media Marks the element that takes the counter scale inside a frame, a wrapper around the picture for instance. It takes no value. Without it the first img, video, picture, canvas, iframe or svg of the frame is taken; a frame with none still opens, without the scale. [ Optional ] data-clip-reveal-state Written by the engine on the frame: ready while it waits closed, playing, done once open, static under reduced motion. Never written by hand. [ Optional ] The value is optional: left empty, the frame opens up. The direction is a setting of the table below as well, and the value written here wins over it. The attribute goes on the box that holds the picture, never on the image itself.SettingsEvery setting is a custom attribute too, added the same way and on the same element. An attribute always wins over the defaults written in the code.Attribute Default What it is data-clip-reveal-color #ffab1a Colour of the rule that leads the edge, the accent of the page. Options: any CSS colour. data-clip-reveal-direction up The way the edge travels. The value written on data-clip-reveal itself wins over this key. Options: up, the frame opens from its bottom edge upwards; down, from the top; left, from the right edge leftwards; right, from the left; center, outwards from the middle, the rule drawn as the outline of the window. data-clip-reveal-duration 1.1 Time a frame takes to open, in seconds, on an exponential out curve; the scrubbed opening ignores it. data-clip-reveal-scale-from 1.2 Scale the picture starts at inside the frame, coming back to 1 on the same curve as the clip. data-clip-reveal-stagger 0.09 Gap between the starts of two frames that come into view together, in seconds, in the order of the page. data-clip-reveal-rule-width 1 Thickness of the accent rule, in px. data-clip-reveal-offset 0.12 Share of the window height the top of a frame has to clear, from the bottom, before it plays, 0 to 0.9. data-clip-reveal-once true Whether a frame opens one time and stays open. Options: true, one opening and the frame is left alone; false, the frame closes once it is fully out of the window and opens again the next time, and a scrubbed frame follows the scroll both ways. data-clip-reveal-scrub false Whether the scroll drives the opening instead of the clock. Options: false, the frame plays by itself as it comes into view; true, the opening follows the scroll over six tenths of the window height, linear and lightly smoothed, with no stagger.
Two files and their pictures (6), no build step.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
ClipReveal.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-clip-reveal-color #ffab1a Colour of the rule that leads the edge, the accent of the page. Options: any CSS colour. data-clip-reveal-direction up The way the edge travels. The value written on data-clip-reveal itself wins over this key. Options: up, the frame opens from its bottom edge upwards; down, from the top; left, from the right edge leftwards; right, from the left; center, outwards from the middle, the rule drawn as the outline of the window. data-clip-reveal-duration 1.1 Time a frame takes to open, in seconds, on an exponential out curve; the scrubbed opening ignores it. Under 0.7 the opening reads as a snap and the long deceleration is lost Past 1.6 the last third looks stalled, since the curve spends it on a few pixels data-clip-reveal-scale-from 1.2 Scale the picture starts at inside the frame, coming back to 1 on the same curve as the clip. At 1 the picture stands still and only the frame opens Past 1.3 the picture visibly zooms out, and a small picture shows its pixels at the start data-clip-reveal-stagger 0.09 Gap between the starts of two frames that come into view together, in seconds, in the order of the page. At 0 a row opens as one block Past 0.15 a row of four reads as a slow queue data-clip-reveal-rule-width 1 Thickness of the accent rule, in px. At 0 no rule is drawn at all Past 2 the rule reads as a bar and takes over the picture data-clip-reveal-offset 0.12 Share of the window height the top of a frame has to clear, from the bottom, before it plays, 0 to 0.9. At 0 a frame opens the moment its first pixel is in the window Past 0.3 a frame opens when it is already well up the screen, and a short page may never get it there data-clip-reveal-once true Whether a frame opens one time and stays open. Options: true, one opening and the frame is left alone; false, the frame closes once it is fully out of the window and opens again the next time, and a scrubbed frame follows the scroll both ways. data-clip-reveal-scrub false Whether the scroll drives the opening instead of the clock. Options: false, the frame plays by itself as it comes into view; true, the opening follows the scroll over six tenths of the window height, linear and lightly smoothed, with no stagger. -
Drive it from JavaScript
When frames arrive after the page has loaded, when a frame has to open from code, or when the page reacts to a frame landing.const element = document.querySelector('[data-clip-reveal]'); element.addEventListener('clip-reveal:done', (e) => { console.log(e.detail); }); const frame = ClipReveal.mount(element); // the frame auto() mounted, or a new one frame.replay(); // closed, then opened again if it is in view frame.show(); // open at once, no motion // Start a video only once its frame opens. element.addEventListener('clip-reveal:start', () => element.querySelector('video')?.play()); // Frames added after the page loaded: mount everything marked inside it. ClipReveal.auto(container); // When the frame leaves the page: frame.destroy(); // observers gone, the frame left visible and bare
-
Fit it to your page
- Structure: one box marked
data-clip-reveal, holding the picture: animg, avideo, apicture, acanvas, aniframeor ansvg, the first one found. To name another element as the picture, a wrapper for instance, mark itdata-clip-reveal-media. A box with no such child still opens; only the counter scale is missing. - The value of the attribute is the direction:
data-clip-reveal="up",down,left,rightorcenter. Left empty, the frame takes thedirectionsetting,upby default. - The box is clipped whole. Everything inside the marked box is hidden until it opens, so a caption that has to stay readable goes next to the box, not inside it. In the preview each caption is a sibling of its frame.
- The frame owes itself a size. The picture fills the frame, not the other way round: the preview's frame has an
aspect-ratio, and its image iswidth: 100%; height: 100%; object-fit: cover. - Rounded corners: give the frame its
border-radiusas usual. While the frame opens, the script reads that radius and writes it inside the clip (inset(... round ...)), since a clip-path ignoresborder-radius. At rest the clip is gone and the stylesheet'soverflow: hiddenon the frame keeps the picture inside the corners. - One setting for every frame of the page, without opening the file: declare
window.ClipRevealSettings = { duration: 1.4 }in a script placed beforeclip-reveal.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-clip-reveal-*attribute on the frame itself (data-clip-reveal-duration="1.4"), so one frame can still differ from the page-wide setting. - Frames that come in together open in a row. They are gathered, their pictures are waited for, and they start in the order of the page, each one
staggerafter the one before. A frame that comes into view a moment later falls in step behind the last one. Nothing to mark: a row is what the scroll brings in at once. - What the script writes:
data-clip-reveal-stateon the frame,readywhile it waits closed,playing,doneonce open,staticunder reduced motion. While a frame waits it carries one inlineopacity: 0; while it plays it carries the clip, awill-change, an inlineposition: relativeif it was static, the scale on its picture, and onespan.clip-reveal-rule. Once open, all of that is taken off: no inline style, no node. - Events, both bubbling, with
detail: { element, direction }:clip-reveal:startwhen a frame starts to open,clip-reveal:donewhen it is open and at rest. - **From JavaScript, beyond
mount,autoanddestroy**: the handle carrieselement,settings,state(),play()which opens the frame now whether it is in view or not,show()which opens it at once with no motion,reset()which closes it again, andreplay()which closes it and plays it again if it is in view.ClipReveal.replay(root)andClipReveal.destroy(root)do the same for every mounted frame insideroot, the document by default. - **
destroy()leaves the frame as it was**: observers and listeners gone, the rule removed, every inline style of the script taken off, the state attribute removed. The frame is then simply visible. - Touch: the same. The effect is driven by the scroll and reads no pointer.
- **
prefers-reduced-motion**: every frame is visible from the start, no clip, no scale, no rule. The state readsstatic. Turning the preference on while the page is open puts every frame at rest at once. - Without JavaScript, the noscript line of the markup shows the frames. If the script is late, a frame waits unseen, keeping its room; if it never comes, the frame shows by itself after four seconds.
- The words and the pictures of the markup are the preview's, and they ship as they are. Put the user's own in their place whenever they want.
- On Webflow, add the fonts first. Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: the paste names them, and the Designer drops a font the site does not have.
- Structure: one box marked
-
Avoid the pitfalls
- Mark the box, not the image. A clip on the image itself cannot hold the counter scale, and the rule has nowhere to live. The attribute goes on the element around the picture.
- **A frame with a
border-radiusandoverflow: visibleloses its corners when it lands.** The radius lives in the clip while the frame opens, and inoverflow: hiddenafterwards. The stylesheet sets it in:where(), so a class of the page that setsoverflow: visiblewins, and the picture's square corners then pop out at the end. - **An ancestor with
overflow: hiddenand a transform does not break it**, but an ancestor that is itselfdisplay: nonewhen the page loads does: a frame that has no box never comes into view. Mount it when it is shown, withClipReveal.auto(container). - Lazy images are waited for. A frame does not open on a blank: it waits for its picture to load, and the frames of its row wait with it, for two and a half seconds at most. A picture that is slow to arrive therefore delays its row, by design.
- **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on a frame that already carries the effect returns the existing handle. A page-wide block goes inwindow.ClipRevealSettings, whichauto()reads itself; to change one frame afterwards,destroy()it, change its attributes, mount it again. - **
scrubon a frame already in view when the page loads** shows it as far open as the scroll position says, which is often fully open: a scrubbed frame belongs further down the page. - **A frame taller than the window, with a high
offset**, opens late: the offset is measured from the top of the frame. - A video opens like a picture, but is not waited for: start it on
clip-reveal:startif it should only play once seen.
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
Clip reveal is a scroll animation for Webflow and vanilla JavaScript, with no library. Also called clip path image reveal, image reveal on scroll or image mask reveal.
The image reveal of a studio site, held to its curve. A box that holds a picture or a video opens as it comes into view: its clip slides from one edge on an exponential curve, half open after a tenth of the time and then one long deceleration, while the picture inside settles from a closer crop on the same curve, so the picture seems to stand still and the frame seems to open. A one pixel rule of the accent rides the travelling edge, placed on the device pixel grid so it never blurs, and dies in a fifth of a second once the edge is all but home. The frame's own corner radius is written inside the clip, so a rounded frame opens rounded from its first pixel. Frames that come into view together wait for their pictures, then open one after the other in the order of the page. One setting hands the opening to the scroll instead of the clock. Five ways for the edge to travel: up, down, left, right, or outwards from the centre, where the rule becomes the outline of the window. No library, no canvas, nothing left on the frame at rest.
Updated
Questions
Add the custom attribute data-clip-reveal to the box around the image, not to the image itself, and write the direction as its value: up, down, left, right or center. Everything inside that box is hidden until it opens, so a caption goes next to the box. Other settings are attributes on the same box, for instance data-clip-reveal-duration="1.4".
No. There is no library and no canvas: the frame opens on a clip-path: inset() while the picture inside settles from a closer crop. To tie the opening to the scroll instead of the clock, write data-clip-reveal-scrub="true", and it follows the scroll over six tenths of the window height.
data-clip-reveal-duration is the time a frame takes to open, 1.1 seconds by default. Under 0.7 the opening reads as a snap, and past 1.6 the last third looks stalled, because the exponential curve spends it on a few pixels. data-clip-reveal-stagger, 0.09 by default, is the gap between two frames that come into view together.
Yes. A frame does not open on a blank: it waits for its picture to load, and the frames of its row wait with it, for two and a half seconds at most. A video opens like a picture but is not waited for.
Yes. A clip-path ignores border-radius, so the script reads the radius of the frame and writes it inside the clip while it opens. Once open the clip is gone and overflow: hidden on the frame holds the corners, so a class that sets overflow: visible lets the square corners of the picture pop out at the end.