Compare Slider
Two images stacked, with a handle that wipes between them. Side-by-side thumbnails make the reader diff from memory; a wipe puts both states in the same pixels, so the difference is the only thing that moves.
src/components/ui/compare-slider.tsxAbout
From the component's own source, where it was written.
Two images stacked, with a handle that wipes between them.
The honest way to show a before and after. Side-by-side thumbnails make the reader do the diffing from memory and lose every small change; a wipe puts the two states in the same pixels, so the difference is the only thing that moves.
The overlay is clipped with inset() rather than resized, which matters: a resized image re-lays-out and re-samples on every pointer move, so a subtle difference gets buried under scaling artefacts and the drag janks. Clipping moves a rectangle over an image that never changes size.
The handle is an <input type="range"> in disguise. It looks like a divider, but it is a real slider, so arrow keys, Home and End all work and it announces itself properly — none of which is true of the div with a pointer handler that this component usually is.
Install
npx astralyx-ui add compare-slider
Usage
import { CompareSlider } from '@/components/ui/compare-slider'
<CompareSlider
before={<img src="/before.jpg" alt="Original" />}
after={<img src="/after.jpg" alt="Upscaled" />}
beforeLabel="Original"
afterLabel="Upscaled"
/>Composer
Change the props and the source below follows.
<CompareSlider
defaultValue={50}
beforeLabel="Original"
afterLabel="Restored"
before={<img src="/before.jpg" alt="Original" />}
after={<img src="/after.jpg" alt="Restored" />}
/>Examples
Before and after
<CompareSlider before={<img src="/before.jpg" alt="" />} after={<img src="/after.jpg" alt="" />} />API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
CompareSlider
Extends Omit<ComponentProps<'div'>, 'onChange'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultValue | string | number | readonly string[] | 50 | Controlled and uncontrolled. |
| after | ReactNode | required | Any node, not just an img — the component stretches whatever you pass to fill the frame. |
| before | ReactNode | required | Any node, not just an img — the component stretches whatever you pass to fill the frame. |
| value | number | — | Controlled and uncontrolled. |
| onValueChange | (value: number) => void | — | Controlled and uncontrolled. |
| ratio | number | 16 / 9 | Reserves the box, so nothing shifts while the images decode. |
| beforeLabel | ReactNode | — | — |
| afterLabel | ReactNode | — | — |
| label | string | 'Compare before and after' | Accessible name for the slider itself. |
| step | number | 1 | Arrow-key step, in percent. |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| clipping, not resizing | clip-path: inset() | The overlay is clipped rather than resized. A resized image re-lays-out and re-samples on every pointer move, which buries subtle differences under scaling artefacts and janks the drag. |
| accessibility | input[type=range] | The handle is a real slider in disguise, so arrows, Home and End work and it announces its position. The visible divider is decoration. |