Compare Slider

Two pictures in one frame and a divider to drag between them, for a redesign, a retouch or a colour grade. A press anywhere glides the divider there; a drag keeps it under the hand.

Mountains above a sea of cloud, ungradedBefore
Mountains above a sea of cloud, gradedAfter

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add compare-slider

Usage#

usage.tsxTSX
import {  CompareSlider,  CompareSliderBefore,  CompareSliderAfter,  CompareSliderHandle,} from "@/components/ui/compare-slider"export function Example() {  return (    <CompareSlider>      <CompareSliderBefore>        <img src="/before.jpg" alt="The old homepage" />      </CompareSliderBefore>      <CompareSliderAfter>        <img src="/after.jpg" alt="The new homepage" />      </CompareSliderAfter>      <CompareSliderHandle />    </CompareSlider>  )}

Anatomy#

import {  CompareSlider,  CompareSliderBefore,  CompareSliderAfter,  CompareSliderHandle,  CompareSliderLabel,} from "@/components/ui/compare-slider"<CompareSlider>  <CompareSliderBefore>    <CompareSliderLabel />  </CompareSliderBefore>  <CompareSliderAfter>    <CompareSliderLabel />  </CompareSliderAfter>  <CompareSliderHandle /></CompareSlider>

Vertical#

orientation="vertical" runs the divider top to bottom, with the before picture on top. Labels move with it: before in the top corner, after in the bottom one. Give the frame a taller ratio, such as aspect-[4/5], for portrait pictures.

Apartment balconies in black and whiteMono
Apartment balconies on a blue and orange facadeColour

Follow the pointer#

With hover, the divider follows a mouse across the frame without a press, easing over a short transition so a shaky hand still reads smooth. On touch it stays a drag, since a finger has no hover. The quiet variant="line" handle suits it: the pointer is already the grip.

A green valley at dawn, ungraded
A green valley at dawn, graded

Handles#

reveal, the default, rests as a small dot threaded on the hairline and unfolds into a pill grip when the frame is hovered, the divider moves or the keyboard lands on it; on a touch screen, with no hover to discover it, the grip stays open. knob keeps a round grip shown at all times; line is the hairline alone. Every line carries an invisible 24px strip, so a finger can take the divider anywhere along it. The handle is a part, not a prop: leave it out and the frame still slides from a press, a tap or the keyboard.

Controlled#

Pass value and onValueChange to drive the divider from outside. A new value glides there instead of jumping, so buttons that show one side or the other feel like the same gesture as a drag.

A green valley at dawn, ungradedBefore
A green valley at dawn, gradedAfter
50%

API reference#

CompareSlider

The frame, a Radix Slider root that spans both pictures: a press anywhere moves the divider. It renders a zero-size thumb that carries role="slider", the value and the keyboard. aspect-video and rounded-xl by default (any aspect-* or h-* class overrides). Forwards Radix Slider root props, and every part accepts className, merged last.

PropTypeDefaultDescription
valuenumber-Where the divider sits, 0 to 100 from the left (or top) edge. Controlled.
defaultValuenumber50Where the divider starts. Uncontrolled.
onValueChange(value: number) => void-Called on every move.
onValueCommit(value: number) => void-Called when a drag ends, a tap lands or a key moves the divider.
orientation"horizontal" | "vertical""horizontal"Which way the divider travels. Vertical puts the before picture on top.
hoverbooleanfalseThe divider follows a mouse over the frame, no press needed. Touch still drags the handle.
keyboardStepnumber5How far an arrow key moves the divider, in percent. Shift + arrow moves 1.
disabledbooleanfalseFreezes the divider and dims the grip.
aria-labelstring"Comparison position"Names the slider thumb for screen readers.
Data attributeDescription
data-moving"drag" while the hand drags the divider, "hover" while it follows the mouse. Absent while a jump glides.
data-orientationhorizontal or vertical.
data-disabledPresent when disabled.
CSS variableDescription
--compare-positionThe divider's position as a percentage. Both clips and the handle read it.

CompareSliderBefore

The picture left of (or above) the divider, clipped at it. A direct <img> or <video> child fills the frame with object-cover. Forwards all native <div> props.

CompareSliderAfter

The picture right of (or below) the divider, clipped at it. Forwards all native <div> props.

CompareSliderHandle

The divider the reader sees: a white hairline with a 24px touch strip and, by default, a round grip whose carets part by a pixel while the frame is hovered. Visual only; focus shows as a brand ring on the grip. Forwards all native <div> props.

PropTypeDefaultDescription
variant"reveal" | "knob" | "line""reveal"reveal rests as a dot on the line and opens into a pill grip on hover, while moving or on keyboard focus (always open on touch). knob keeps a round grip shown; line drops the grip for a quieter frame.
revealedbooleanfalseKeeps the reveal grip open at rest.
childrenReact.ReactNode-Replaces the two carets inside the grip.

CompareSliderLabel

A chip naming the picture, placed inside CompareSliderBefore or CompareSliderAfter. It pins to that picture's outer corner and fades out while the divider passes over it. Forwards all native <span> props.

Data attributeDescription
data-coveredPresent while the divider is over the label.

FAQ#

Yes. A finger that lands on the picture and moves is left to scroll the page; a short tap moves the divider there. Dragging starts from the handle's line or grip, which take the gesture for themselves.