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.
Installation#
Usage#
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.
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.
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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | - | Where the divider sits, 0 to 100 from the left (or top) edge. Controlled. |
| defaultValue | number | 50 | Where 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. |
| hover | boolean | false | The divider follows a mouse over the frame, no press needed. Touch still drags the handle. |
| keyboardStep | number | 5 | How far an arrow key moves the divider, in percent. Shift + arrow moves 1. |
| disabled | boolean | false | Freezes the divider and dims the grip. |
| aria-label | string | "Comparison position" | Names the slider thumb for screen readers. |
| Data attribute | Description |
|---|---|
| data-moving | "drag" while the hand drags the divider, "hover" while it follows the mouse. Absent while a jump glides. |
| data-orientation | horizontal or vertical. |
| data-disabled | Present when disabled. |
| CSS variable | Description |
|---|---|
| --compare-position | The 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| 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. |
| revealed | boolean | false | Keeps the reveal grip open at rest. |
| children | React.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 attribute | Description |
|---|---|
| data-covered | Present while the divider is over the label. |