PixelDosa UI

Compare View

Before and after an AI edit — with a slider that actually works from the keyboard.

Sky replaced

Original photo, overcast skyAI edit, clear sky at golden hour

Installation

bash
npx shadcn@latest add @pixeldosa/compare-view

Requires `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

PropTypeNotes
before / after{ content, label }Required. label doubles as the description.
view"slider" | "side"Starting view. Defaults to "slider".
allowViewChangebooleanDefaults to true.
onChoose(choice) => voidAdds "Keep the new one" / "Keep the original".
chooseAfterLabel / chooseBeforeLabelstringSay what keeping means.
aspectRatiostringDefaults to "4 / 3".
titlestringDefaults to "Before and after".
headingLevel2 | 3 | 4 | 5 | 6Defaults to 3.

Accessibility

  • The slider is a labelled range input: "Slide to compare original photo with AI edit".
  • aria-valuetext names 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 figure and figcaption, 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 Versions for 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.