Image Compare
Drag the divider in the middle to compare a before and an after layer, clipped with clip-path and following smoothly on a spring.
npx shadcn@latest add https://webberui.com/r/image-compare.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ImageCompare />
Installation
npx shadcn@latest add https://webberui.com/r/image-compare.jsonUsage
before / after accept any ReactNode — most commonly an image, but a video, a code block, or an entire stretch of UI all work. before is the lower layer, shown to the left of the divider; after is the upper layer, clipped by the divider position and shown to the right. It is best to give the container a size in className (such as aspect-video) and let both layers fill it with h-full w-full:
import { ImageCompare } from "@/components/ui/image-compare";
<ImageCompare
before={
<img src="/photo-before.jpg" alt="Before grading" className="h-full w-full object-cover" />
}
after={
<img src="/photo-after.jpg" alt="After grading" className="h-full w-full object-cover" />
}
beforeLabel="Before"
afterLabel="After"
initial={40}
onChange={(value) => console.log(value)}
className="aspect-video w-full max-w-2xl rounded-xl"
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
before | ReactNode | — | The lower layer (left of the divider), which sizes the container in the document flow |
after | ReactNode | — | The upper layer (right of the divider), clipped with clip-path by the divider position |
initial | number | 50 | Initial divider position (0–100 percent) |
beforeLabel | ReactNode | — | Top-left label: fades in on reveal and fades out to get out of the way as the divider is dragged close |
afterLabel | ReactNode | — | Top-right label: fades in on reveal and fades out to get out of the way as the divider is dragged close |
onChange | (value: number) => void | — | Callback when the divider position changes (0–100) |
className | string | — | Appended to the outermost container; it is best to set the size and corner radius here |
Accessibility
- The handle is a focusable
role="slider"element witharia-valuemin/aria-valuemax/aria-valuenow/aria-valuetext, so assistive technology announces the current split percentage live - Keyboard operation: once the handle is focused with Tab, ← / ↓ subtract 2%, → / ↑ add 2%, Home jumps to 0, and End jumps to 100
- When the user has "reduce motion" enabled at the system level, the divider does not smooth through a spring and jumps straight to the target position, and the labels do not play their reveal animation either
How it works
- Smooth spring tracking: the divider position lives in a
useMotionValue(a percentage), and the on-screen value is smoothed withuseSpringbefore drivingclip-path: inset()and the divider'sleft, which gives dragging a bit of trailing momentum - The whole container is the hot zone: pressing the pointer anywhere jumps to that position and starts dragging (
setPointerCapturekeeps it from breaking off when you drag outside the container), so you never have to hit the handle precisely - Touch support: the handle sets
touch-action: none, so dragging from it on a phone does not accidentally scroll the page; the container also intercepts the native image drag (dragstart) to avoid interrupting the gesture - Labels getting out of the way: the opacity of the two corner labels is driven by the divider position — they fade out progressively as the divider gets close, within the 8–22% (left) or 78–92% (right) range, so they never cover the content being compared
- The animation runs only on
clip-pathand transform, so it never triggers layout
Audio Visualizer
Web Audio API audio spectrum visualization drawn in canvas 2D, in bar, mirror, and radial styles, able to analyze a real audio source or drive a decorative animation from synthetic data.
Zoom Lens
An image magnifier whose lens follows the cursor and magnifies in place, or presents the magnified result in a side panel.