Skip to content
WANCRAFT

B.40Cards & Containers

Compare Slider

A before / after comparison with a draggable, keyboard-operable divider revealed by clip-path.

NewReactMotionCSSTailwindintermediate

Overview

Compare Slider stacks two images (or any elements) and reveals the before layer up to a divider you can drag, click to, or move with the keyboard. The reveal is a clip-path driven by a motion value and the divider is a full-size layer moved with a transform, so dragging causes no layout work and no React renders. It splits horizontally or vertically, can follow a hovering mouse, fades its labels as the divider approaches them, and exposes the handle as a proper role="slider".

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/compare-slider.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/compare-slider.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/compare-slider.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { CompareSlider } from "@/components/ui/compare-slider";

export function CompareSliderExample() {
  return (
    <CompareSlider
      before="/images/kitchen-before.jpg"
      after="/images/kitchen-after.jpg"
      className="aspect-[16/10] w-full max-w-2xl"
    />
  );
}

03Props

PropTypeDefaultDescription
before / afterReactNode—Required. A string is treated as an image URL (rendered as <img> with the label as alt); anything else is rendered as-is.
beforeLabel / afterLabelstring"Before" / "After"Chip text, image alt text and part of aria-valuetext.
showLabelsbooleantrueShow the label chips (they fade as the divider approaches).
orientation"horizontal" | "vertical""horizontal"Side by side, or top / bottom.
position / defaultPositionnumber— / 50Controlled or initial divider position in %.
onPositionChange(position: number) => void—Called with the new position in % on every change.
followPointerbooleanfalseMove the divider by hovering with a mouse (touch still drags).
stepnumber1Arrow-key step in %. Page Up / Down move 10%.
accentstring"#FF7448"Handle ring and focus colour.
roundednumber16Corner radius in px.
handleLabelstring"Comparison divider"Accessible name of the handle.
...propsHTMLAttributes<HTMLDivElement>—Forwarded to the root; use className to size it (e.g. an aspect ratio).

04Features

  • 01clip-path reveal driven by a motion value — no layout thrash, no re-renders while dragging
  • 02Press anywhere to jump, then drag; pointer capture keeps the drag alive
  • 03Optional hover-to-scrub mode for mice
  • 04Horizontal and vertical orientation
  • 05Labels fade out as the divider reaches them
  • 06Image URLs or arbitrary elements (SVG, canvas, video, components)
motion · springinput · draginput · pointerinput · hoverinput · keyboardinput · focus

05Accessibility

  • The handle is a focusable role="slider" with aria-orientation, aria-valuenow and an aria-valuetext such as “Before 40%, After 60%”.
  • Arrow keys move the divider, Page Up / Page Down move 10%, Home / End jump to either side.
  • Images use the labels as alt text; the visual label chips are aria-hidden to avoid duplication.
  • Touch scrolling along the other axis keeps working (touch-action pan-y / pan-x).
  • With prefers-reduced-motion keyboard moves jump instead of springing; there are no other animations.

06Limitations

  • Strings are always treated as image URLs; wrap plain text in an element.