Compare slider
A before and after comparison with weight.
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 tutorialGive the box a width and both pictures the same size: the first picture gives the box its height, the second is stretched over it. The box clips what sticks out of it, so a shadow goes on a wrapper around it. -
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-compare-slider Marks the box that holds the two pictures. It takes no value: leave the value field empty. The first picture in the box is the one before, the second the one after, and the handle is added to that box, which also carries the settings. Without it on any box of the page nothing is mounted, and the embed says so in the console. data-compare-slider-picture Marks the two elements to compare when they are not images, videos or canvases, two blocks with a background for one: the first marked is before, the second after. Without it the first two pictures of the box are taken, and the engine writes the mark on them itself, before and after. [ Optional ] data-compare-slider-state Written by the engine on the box: live while the split is moving, idle at rest, still under reduced motion. Never written by hand. [ Optional ] data-compare-slider-axis Written by the engine on the box from the orientation setting: x for a split that travels left and right, y for one that travels up and down. Never written by hand; the stylesheet reads it to turn the clip, the rule and the tags. [ Optional ] 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-compare-slider-color #ffab1a Colour of the rule and of the ground of its two tags. Options: any CSS colour, the amber #ffab1a by default. data-compare-slider-label-color #15100c Colour of the words on the two tags, read against color. Options: any CSS colour, the ink #15100c on the amber by default. data-compare-slider-rest 50 Where the split sits when nobody is on the box, 0 to 100, and where it glides back to when the pointer leaves. data-compare-slider-stiffness 140 Pull of the spring that carries the split to the pointer. data-compare-slider-damping 16 Brake of the spring, read against stiffness. data-compare-slider-intro true Whether the split plays one short sweep the first time the box enters the window. Options: true plays it once, when the box is a third in view and its pictures are loaded; false leaves the split at rest. data-compare-slider-follow true Whether the split follows a mouse that only passes over the box. Options: true follows the pointer and glides back to rest when it leaves; false moves on a press or a drag only, and stays where it is left. data-compare-slider-orientation horizontal The way the split travels. Options: horizontal, a vertical rule that moves left and right, the first picture on the left; vertical, a horizontal rule that moves up and down, the first picture on top. data-compare-slider-before Before Word of the tag on the side of the first picture, also what a screen reader calls that picture. Options: any short word; empty takes the tag away. data-compare-slider-after After Word of the tag on the side of the second picture, also what a screen reader calls that picture. Options: any short word; empty takes the tag away.
Two files and their pictures (2), 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
CompareSlider.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-compare-slider-color #ffab1a Colour of the rule and of the ground of its two tags. Options: any CSS colour, the amber #ffab1a by default. data-compare-slider-label-color #15100c Colour of the words on the two tags, read against color. Options: any CSS colour, the ink #15100c on the amber by default. data-compare-slider-rest 50 Where the split sits when nobody is on the box, 0 to 100, and where it glides back to when the pointer leaves. Towards 0 the box rests on the second picture Towards 100 the box rests on the first picture data-compare-slider-stiffness 140 Pull of the spring that carries the split to the pointer. Under 80 the rule trails far behind and feels heavy Over 300 it sticks to the pointer and the lag is gone data-compare-slider-damping 16 Brake of the spring, read against stiffness. Under 10 the rule swings past its target several times Over 26 no overshoot at all, the rule only eases in data-compare-slider-intro true Whether the split plays one short sweep the first time the box enters the window. Options: true plays it once, when the box is a third in view and its pictures are loaded; false leaves the split at rest. data-compare-slider-follow true Whether the split follows a mouse that only passes over the box. Options: true follows the pointer and glides back to rest when it leaves; false moves on a press or a drag only, and stays where it is left. data-compare-slider-orientation horizontal The way the split travels. Options: horizontal, a vertical rule that moves left and right, the first picture on the left; vertical, a horizontal rule that moves up and down, the first picture on top. data-compare-slider-before Before Word of the tag on the side of the first picture, also what a screen reader calls that picture. Options: any short word; empty takes the tag away. data-compare-slider-after After Word of the tag on the side of the second picture, also what a screen reader calls that picture. Options: any short word; empty takes the tag away. -
Drive it from JavaScript
When something other than the pointer should move the split, a button or a scroll position, or when the page reacts to the value it comes to rest on.const element = document.querySelector('[data-compare-slider]'); element.addEventListener('compareslider:change', (e) => { console.log(e.detail); }); const slider = CompareSlider.mount(element); // the box auto() mounted, or a new one slider.set(80); // springs the split to 80 slider.set(20, true); // or jumps there slider.replay(); // back to rest, then the intro again element.addEventListener('compareslider:change', (e) => { console.log(e.detail.value); // 0 to 100, once the split is at rest }); // When the box leaves the page: slider.destroy(); // removes the handle and the listeners
-
Fit it to your page
- Structure: one box marked
data-compare-slider, with two pictures of the same size as its first two children. The first is the picture before, shown whole; the second is the picture after, laid over the first and clipped at the split. The script adds one element, the handle, as the last child of the box. Anything outside the box is yours; the effect reads no class. - What counts as a picture: an
img, apicture, avideoor acanvas, child of the box. To use anything else, adivwith a background for one, mark the two elementsdata-compare-slider-picture: the first marked is before, the second after. - The box needs a width, the pictures a common size. The first picture stays in the flow and gives the box its height; the second is stretched over it with
object-fit: cover. The preview's box isaspect-ratio: 4 / 5and both pictures fill it. - One setting for every box of the page, without opening the file: declare
window.CompareSliderSettings = { rest: 35 }in a script placed beforecompare-slider.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-compare-slider-*attribute on the box itself (data-compare-slider-rest="35"), so one box can still differ from the page-wide setting. - The two words are settings like the others:
data-compare-slider-before="Ivory Laid"anddata-compare-slider-after="Kraft Natural"on the box. An empty value takes that tag away, and the rule runs bare. - The pointer: with
followon, a mouse over the box carries the split, and leaving sends it back torest. A press takes the rule for a drag, which goes on outside the box until the button is let go. Withfollowoff, only a press or a drag moves the split, and it stays where it is left. - Touch: a finger drags the rule along its axis; a swipe the other way scrolls the page as usual, and the split does not move. A tap sends the split to the point tapped. On touch the split always stays where it is left.
- Keyboard: the handle is in the tab order. The arrow keys move the split by 2, with Shift or the page keys by 10, Home and End send it to either edge. A value set from the keyboard stays.
- What a screen reader gets:
role="slider", anaria-labelmade of the two words,aria-orientation,aria-valuenowfrom 0 to 100 and anaria-valuetextthat says the share of each picture. Give both pictures analt. - What the script writes on the box:
data-compare-slider-state,livewhile the split is moving,idleat rest,stillunder reduced motion;data-compare-slider-axis,xory; and three custom properties in its style,--compare-slider-at, the split as a percentage, and the two colours. No size and no position in pixels: the split is a share of the box, so a box that changes size keeps it. - **From JavaScript, beyond
mount,autoanddestroy**: the api carrieselement,settings,handle,value()andposition(), where the split is going and where it is,set(value, jump),replay()which plays the intro again,refresh()which reads the settings again, andframes()andisRunning()for a test that wants to see the loop sleep.CompareSlider.replay()plays the intro on every box of the page. The box firescompareslider:changeeach time the split comes to rest on a new value. - **
destroy()leaves the box as it was**: the handle removed, the listeners and the observer gone, the attributes and the custom properties taken off, and theloadinganddraggableof the two pictures put back. - **
prefers-reduced-motion**: no spring and no intro. The split jumps to the pointer, to the key pressed, and back to rest; the page is complete and the slider fully usable. The state readsstill, and turning the preference off brings the spring back without a reload. - The words of the markup are the preview's, and they ship as they are. Put the user's own words 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
- Two pictures of different ratios do not line up. The second is stretched over the box of the first with
object-fit: cover, so it is cropped, never squeezed, but the two only register when they share a ratio. Export both at the same size. - **Without
compare-slider.cssthe second picture sits under the first.** The stack and the clip are the stylesheet's; the script only moves one custom property. - The box clips. It is
overflow: hidden, which is what lets a tag leave with the picture it names. A shadow or a badge that should stick out of the frame goes on a wrapper around the box. - The handle covers the box. It is the last child and takes the pointer, so a link or a button inside the box cannot be clicked. Captions and actions go beside the box, not in it.
- A carousel or a horizontal scroller around the box loses the swipe. The box keeps the swipe along its own axis (
touch-action: pan-y, orpan-xwhen vertical). Inside a horizontal carousel, make the slider vertical, or turnfollowoff and keep it out of the swipe area. - **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on a box that already carries the effect returns the existing one. A page-wide block goes inwindow.CompareSliderSettings, whichauto()reads itself; a box already mounted takes new attributes throughrefresh(). - **
dampingat 0 never settles.** The spring then swings for ever and the loop never sleeps. Keep it over 6. - The tags are set in Geist Mono. The stylesheet names it with a fallback stack, so a page without the font shows them in the system mono.
- Two pictures of different ratios do not line up. The second is stretched over the box of the first with
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
Compare slider is a cursor animation for Webflow and vanilla JavaScript, with no library. Also called before after slider, image comparison slider or before and after image slider.
A before and after comparison with weight. Two pictures of the same size share one box, the second laid over the first and clipped at a split, and a thin rule marks the split with a small mono tag on each side. The rule follows the pointer on a spring, so it lags a little and overshoots a little before it settles, and it glides back to its rest position when the pointer leaves. A finger drags it while the page still scrolls the other way, and the handle is a real slider: it takes the focus, the arrow keys move it, and a screen reader hears its value. As the box enters the window the split plays one short sweep, so the frame says it moves before anyone touches it. Horizontal or vertical, no library, and no loop at rest. Under reduced motion nothing springs: the split jumps to where it is asked. Pattern type before and after slider, image comparison, image reveal.
Updated
Questions
Add the custom attribute data-compare-slider to a box whose first two children are the two pictures, the before one first; the script adds the handle. Settings are attributes on the box, for instance data-compare-slider-rest="35", and the two words on the rule are data-compare-slider-before and data-compare-slider-after. Export both pictures at the same size, because two different ratios do not line up.
No. It has no library and no build step: the second picture is cut by clip-path: inset() from one custom property, --compare-slider-at, and the rule is carried by a spring written in the engine. The loop stops as soon as the split has settled and does not exist at rest.
Yes, and it is the default: with follow on, a mouse over the box carries the split, and leaving sends it back to rest, 50 by default. Write data-compare-slider-follow="false" and only a press or a drag moves the split, which then stays where it is left. On touch a finger drags the rule along its axis, and a swipe the other way scrolls the page as usual.
Write data-compare-slider-orientation="vertical": the rule turns horizontal and moves up and down, with the first picture on top. The default is horizontal, the first picture on the left. Inside a horizontal carousel the vertical one is the safe choice, since the box keeps the swipe along its own axis.
Yes. The handle carries role="slider" and is in the tab order: the arrow keys move the split by 2, Shift or the page keys by 10, and Home and End send it to either edge. A screen reader gets aria-valuenow from 0 to 100 and an aria-valuetext that says the share of each picture, so give both pictures an alt.