Split Compare
The same component in both its themes, one over the other, and a 'VS' divider you drag to uncover one or the other: by mouse, by finger or with the arrow keys, thanks to an invisible native slider laid over the whole scene. 0.45 KB of JavaScript; without the script, the comparison stays fixed at 50%.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Transitions has 52 effects, including 9 free. Explore the category →
3 usage examples



How it works
The scene .th-split-scene (300 × 220 px, 16 px corners) overlaps two layers of its exact size with position: absolute; inset: 0: .th-split-dark (background #0f172a) below and .th-split-light (background #f8fafc) on top. Each holds the same component in the same place — here a 180 px card with a title and a progress bar, set at the bottom — and its label, 'Dark' top left, 'Light' top right. The light layer is clipped from the left up to the divider: clip-path: inset(0 0 0 var(--th-split-pos)), with --th-split-pos: 50% by default. At 50%, the card appears cut in two, half dark, half light: the comparison happens on a single object.
The gesture goes through a real <input type="range"> from 0 to 100, stretched over the whole scene (inset: 0, z-index 3), made invisible (opacity: 0, appearance: none, 2 px native thumb) but still active, with an ew-resize mouse cursor. The browser supplies everything else: press anywhere to jump there, drag, touch, keyboard. On each input event, the script writes the value into --th-split-pos. The visible divider is a scene-wide layer that ignores the pointer, shifted by translateX(var(--th-split-pos)): its ::before draws the 2 px violet-to-pink line on its left edge, and the 'VS' knob is centered on that edge, flanked by two small arrows that say it moves. No transition: the divider follows the gesture with no lag.
Measured in Chromium 151, Firefox 153 and WebKit 26: dragged from 50 to 20 then 85%, exact values and the knob center at 60 then 255 px; a single click at 30% jumps there; right arrow +1, Page Up +10, Home 0, End 100; in Chromium and Firefox, the Tab key reaches the slider and the white outline shows around the knob. By finger, in Chromium mobile emulation: a tap at 25% gives 25, a drag from 25 to 75% gives 75. The script (0.45 KB) walks through every .th-split-scene on the page: several comparers coexist. Without JavaScript, the scene keeps its fixed 50% cut. The effect compares two themes, it applies none: it does not read prefers-color-scheme and remembers nothing.
Accessibility
- Native slider: an
input type="range"announced as a slider (sliderrole) with its value from 0 to 100 and a name that says what is where: 'Divider: dark theme on the left, light theme on the right'. Keyboard measured in all three engines: arrows (1%), Page Up / Page Down (10%), Home and End. Keyboard focus draws a 2 px white outline around the knob, since the slider itself is invisible. - Duplicated content: both layers carry the same text; the light layer is
aria-hidden, so a screen reader reads only one version. If the comparison carries information (a logo unreadable in dark mode, for instance), also state it in text nearby. - Motion: nothing moves on its own; the divider only moves under the finger, the mouse or the keyboard, with no animation. No reduced-motion rule is therefore needed.
- Contrast and touch: 'Dark' label at 6.1:1, 'Light' at 4.6:1; the 'VS' knob is decorative. The slider covers the whole comparer: on a phone, a vertical swipe started on it may be captured by the slider instead of scrolling the page (not measured on a real device); leave some margin around it for scrolling.
Browser compatibility
CSS (clip-path: inset() with a variable, transform, pseudo-elements, slider styling via ::-webkit-slider-thumb and ::-moz-range-thumb, :focus-visible) + 0.45 KB of JavaScript, no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: the same values by mouse and keyboard, the same knob position to the pixel; touch measured in Chromium mobile emulation. Unprefixed clip-path: inset() exists since Chrome 55, Firefox 54 and Safari 9.1; custom properties since Chrome 49, Firefox 31 and Safari 9.1. The minimum versions come from inset: 0, which stacks the layers: Chrome and Edge 87, Firefox 66, Safari 14.1 (iOS 14.5).
Without JavaScript, the scene stays cut at 50%: both themes side by side, the slider moves but the divider does not follow. Without clip-path, the light layer covers the whole scene.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="th-split-root">
<div class="th-split-scene">
<div class="th-split-half th-split-dark">
<span class="th-split-label">…</span>
<div class="th-split-mini-card">…</div>
</div>
<div class="th-split-half th-split-light" aria-hidden="true">
<span class="th-split-label">…</span>
<div class="th-split-mini-card">…</div>
</div>
<input class="th-split-range" type="range" min="0" max="100" step="1" value="50" aria-label="…">
<div class="th-split-divider" aria-hidden="true"><span class="th-split-knob">VS</span></div>
</div>
</div>
The full HTML + CSS + JS of Split Compare, ready to paste — plus all 840 effects. Or $299 once, lifetime access.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Starting position (HTML — the slider's value, and CSS — --th-split-pos) |
50 | Put the same value in both places (value="30" and --th-split-pos: 30%) so it also holds without JavaScript. |
Keyboard step (HTML — step) |
1 (1% per arrow, 10% per page) | step="5" makes the keyboard faster but moves the drag in 5% notches. Keep 1 for a smooth drag. |
Layer content (HTML — .th-split-dark and .th-split-light) |
a 180 px card at the bottom | Both layers must show the same content in the same place, otherwise the cut compares nothing. Two images of the same size (object-fit: cover) make a before / after photo. |
Line and knob (CSS — .th-split-divider::before, .th-split-knob) |
2 px #8b5cf6 → #ec4899 line, 'VS' knob |
Replace 'VS' with two chevrons or an icon; keep the knob at mid-height, where the thumb looks for it. |
Colors and labels (CSS — both layer backgrounds, .th-split-label) |
#0f172a / #f8fafc, 'Dark' / 'Light' |
Also change the slider's name (aria-label) if the two sides are no longer themes. |
FAQ
--th-split-pos, in 0.45 KB. The slider is made invisible but keeps its focus: the knob shows the outline.prefers-color-scheme. The position is in range.value at any time, but a position is not a choice: add two buttons ('Keep dark', 'Keep light') as in the first example, and apply and remember the chosen theme with your own code (a class on <html>, localStorage).clip-path: inset() and draw a line on the border. Wipe Transition plays the change by itself in 0.6 s, from one theme to the other, and stops on the new one. Split Compare plays nothing: the user holds the border and leaves it wherever they want, for as long as it takes to compare. Choose Wipe Transition to switch, Split Compare to decide.