Installation
npx shadcn@latest add @pixeldosa/compare-viewRequires `cn` at `@/lib/utils`. Write both `label`s as real descriptions: they are the accessible text for each side.
Usage
"use client";
import { CompareView } from "@/components/ui/compare-view";
<CompareView
title="Sky replaced"
aspectRatio="16 / 9"
before={{ label: "Original photo, overcast sky", content: <img src={original} alt="" /> }}
after={{ label: "AI edit, clear sky at golden hour", content: <img src={edited} alt="" /> }}
onChoose={keep}
/>
Why it exists
Comparison sliders are everywhere in AI editing tools, and nearly all of them are mouse-only: a drag handle built from pointer maths, no keyboard, no announcement, often no description of either image.
Features
- A real
input[type=range]. Arrow keys, Home and End, touch and screen readers come from the platform, not from custom code. - It says what's being revealed: "60% AI edit, clear sky at golden hour".
- Clipped, not resized, so both halves stay pixel-aligned where people are looking for differences.
- Side by side is one click away, for anyone who'd rather see both at once.
- Labels are required on both sides, because an unlabelled comparison means nothing to anyone not looking at it.
Props
| Prop | Type | Notes |
|---|---|---|
before / after | { content, label } | Required. label doubles as the description. |
view | "slider" | "side" | Starting view. Defaults to "slider". |
allowViewChange | boolean | Defaults to true. |
onChoose | (choice) => void | Adds "Keep the new one" / "Keep the original". |
chooseAfterLabel / chooseBeforeLabel | string | Say what keeping means. |
aspectRatio | string | Defaults to "4 / 3". |
title | string | Defaults to "Before and after". |
headingLevel | 2 | 3 | 4 | 5 | 6 | Defaults to 3. |
Accessibility
- The slider is a labelled range input: "Slide to compare original photo with AI edit".
aria-valuetextnames how much of the new image is showing — "40% AI edit showing" — not a bare number. The handle is the divider, so the announcement counts down as the value goes up; reading the raw value out would tell a screen-reader user the opposite of what is on screen.- Side-by-side uses
figureandfigcaption, so each side keeps its description. - Nothing animates, so the handle never lags behind the pointer or the arrow key.
When not to use
- Several results from one prompt: use
Variation Grid. - Rewritten text: use
Diff Accept, which shows what changed word by word. - Three or more versions: this is a pair. Use
Response Versionsfor a history.
why it's built this way
Engineering Notes
The slider is a real range input for one reason that outweighs the styling cost: every keyboard, touch and assistive-technology behaviour comes from the platform, and the custom handles these tools ship are the single most common accessibility failure in AI editing UIs. Revealing is done with `clip-path` on an absolutely positioned overlay rather than by resizing the after side, because resizing re-lays-out the media and the two halves drift apart by a pixel or two at every step — visible exactly where people are looking for differences. `aria-valuetext` counts down from the slider value — the handle is the divider, so a higher value uncovers *less* of the new image — and names the side it is measuring, so the announcement matches the screen instead of contradicting it. Labels are required rather than optional on both sides, since a comparison whose halves are unlabelled is meaningless to anyone not looking at it.
how it moves, and when it doesn't
Motion Notes
Nothing animates. The handle follows the input directly with no easing, because a comparison is a direct-manipulation surface where a lagging reveal reads as broken rather than smooth. The view toggle changes layout instantly for the same reason, and `prefers-reduced-motion` has nothing to disable.