Before / After Slider
Drag (or use arrow keys) to compare two images or designs.
LumoraProduct · Pricing · Docs
Design systems that scale with you
Tokens, components and docs — in sync.
Get startedcomponents/ui/image-compare.tsx
"use client";
import { ChevronsLeftRight } from "lucide-react";
import { useState, type ReactNode } from "react";
type ImageCompareProps = {
before: ReactNode;
after: ReactNode;
beforeLabel?: string;
afterLabel?: string;
/** Starting position, 0 - 100 */
initial?: number;
className?: string;
};
export function ImageCompare({ before, after, beforeLabel = "Before", afterLabel = "After", initial = 50, className = "" }: ImageCompareProps) {
const [pos, setPos] = useState(initial);
return (
<div className={`relative aspect-[16/10] w-full select-none overflow-hidden rounded-2xl border border-zinc-200 ${className}`}>
<div className="absolute inset-0">{after}</div>
<div className="absolute inset-0" style={{ clipPath: `inset(0 ${100 - pos}% 0 0)` }}>
{before}
</div>
<span className="absolute left-3 top-3 rounded-full bg-zinc-900/80 px-2.5 py-1 text-xs font-medium text-white">{beforeLabel}</span>
<span className="absolute right-3 top-3 rounded-full bg-white/90 px-2.5 py-1 text-xs font-medium text-zinc-900 shadow-sm">{afterLabel}</span>
<div className="pointer-events-none absolute inset-y-0 w-0.5 -translate-x-1/2 bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.08)]" style={{ left: `${pos}%` }}>
<span className="absolute left-1/2 top-1/2 grid size-10 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full bg-white text-zinc-700 shadow-lg ring-1 ring-zinc-200">
<ChevronsLeftRight className="size-4" />
</span>
</div>
<input
type="range"
min={0}
max={100}
step={0.5}
value={pos}
onChange={(e) => setPos(Number(e.target.value))}
aria-label="Comparison slider"
className="absolute inset-0 h-full w-full cursor-ew-resize opacity-0"
/>
</div>
);
}
function Wireframe() {
return (
<div className="flex h-full flex-col gap-3 bg-zinc-100 p-6">
<div className="flex justify-between">
<span className="h-4 w-20 rounded bg-zinc-300" />
<span className="h-4 w-32 rounded bg-zinc-300" />
</div>
<span className="mt-6 h-7 w-3/4 rounded bg-zinc-300" />
<span className="h-7 w-1/2 rounded bg-zinc-300" />
<span className="mt-2 h-3 w-2/3 rounded bg-zinc-200" />
<span className="mt-3 h-9 w-32 rounded-lg border-2 border-dashed border-zinc-300" />
<div className="mt-auto grid grid-cols-3 gap-3">
{[0, 1, 2].map((i) => (
<span key={i} className="h-16 rounded-lg border-2 border-dashed border-zinc-300" />
))}
</div>
</div>
);
}
function Final() {
return (
<div className="flex h-full flex-col gap-3 bg-gradient-to-br from-white via-blue-50 to-violet-100 p-6">
<div className="flex justify-between">
<span className="text-sm font-semibold text-zinc-900">Lumora</span>
<span className="text-xs text-zinc-500">Product · Pricing · Docs</span>
</div>
<p className="mt-6 text-2xl font-semibold leading-tight tracking-tight text-zinc-900 sm:text-3xl">Design systems that scale with you</p>
<p className="text-sm text-zinc-500">Tokens, components and docs — in sync.</p>
<span className="mt-1 w-fit rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white">Get started</span>
<div className="mt-auto grid grid-cols-3 gap-3">
{["from-sky-400 to-blue-600", "from-violet-400 to-indigo-600", "from-pink-400 to-rose-600"].map((c) => (
<span key={c} className={`h-16 rounded-lg bg-gradient-to-br ${c}`} />
))}
</div>
</div>
);
}
export default function Demo() {
return (
<div className="w-full max-w-xl">
<ImageCompare before={<Wireframe />} after={<Final />} beforeLabel="Wireframe" afterLabel="Final" />
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/image-compare.tsxcomponents/ui/image-compare.tsx"use client"; import { ChevronsLeftRight } from "lucide-react"; import { useState, type ReactNode } from "react"; type ImageCompareProps = { before: ReactNode; after: ReactNode; beforeLabel?: string; afterLabel?: string; /** Starting position, 0 - 100 */ initial?: number; className?: string; }; export function ImageCompare({ before, after, beforeLabel = "Before", afterLabel = "After", initial = 50, className = "" }: ImageCompareProps) { const [pos, setPos] = useState(initial); return ( <div className={`relative aspect-[16/10] w-full select-none overflow-hidden rounded-2xl border border-zinc-200 ${className}`}> <div className="absolute inset-0">{after}</div> <div className="absolute inset-0" style={{ clipPath: `inset(0 ${100 - pos}% 0 0)` }}> {before} </div> <span className="absolute left-3 top-3 rounded-full bg-zinc-900/80 px-2.5 py-1 text-xs font-medium text-white">{beforeLabel}</span> <span className="absolute right-3 top-3 rounded-full bg-white/90 px-2.5 py-1 text-xs font-medium text-zinc-900 shadow-sm">{afterLabel}</span> <div className="pointer-events-none absolute inset-y-0 w-0.5 -translate-x-1/2 bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.08)]" style={{ left: `${pos}%` }}> <span className="absolute left-1/2 top-1/2 grid size-10 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full bg-white text-zinc-700 shadow-lg ring-1 ring-zinc-200"> <ChevronsLeftRight className="size-4" /> </span> </div> <input type="range" min={0} max={100} step={0.5} value={pos} onChange={(e) => setPos(Number(e.target.value))} aria-label="Comparison slider" className="absolute inset-0 h-full w-full cursor-ew-resize opacity-0" /> </div> ); } function Wireframe() { return ( <div className="flex h-full flex-col gap-3 bg-zinc-100 p-6"> <div className="flex justify-between"> <span className="h-4 w-20 rounded bg-zinc-300" /> <span className="h-4 w-32 rounded bg-zinc-300" /> </div> <span className="mt-6 h-7 w-3/4 rounded bg-zinc-300" /> <span className="h-7 w-1/2 rounded bg-zinc-300" /> <span className="mt-2 h-3 w-2/3 rounded bg-zinc-200" /> <span className="mt-3 h-9 w-32 rounded-lg border-2 border-dashed border-zinc-300" /> <div className="mt-auto grid grid-cols-3 gap-3"> {[0, 1, 2].map((i) => ( <span key={i} className="h-16 rounded-lg border-2 border-dashed border-zinc-300" /> ))} </div> </div> ); } function Final() { return ( <div className="flex h-full flex-col gap-3 bg-gradient-to-br from-white via-blue-50 to-violet-100 p-6"> <div className="flex justify-between"> <span className="text-sm font-semibold text-zinc-900">Lumora</span> <span className="text-xs text-zinc-500">Product · Pricing · Docs</span> </div> <p className="mt-6 text-2xl font-semibold leading-tight tracking-tight text-zinc-900 sm:text-3xl">Design systems that scale with you</p> <p className="text-sm text-zinc-500">Tokens, components and docs — in sync.</p> <span className="mt-1 w-fit rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white">Get started</span> <div className="mt-auto grid grid-cols-3 gap-3"> {["from-sky-400 to-blue-600", "from-violet-400 to-indigo-600", "from-pink-400 to-rose-600"].map((c) => ( <span key={c} className={`h-16 rounded-lg bg-gradient-to-br ${c}`} /> ))} </div> </div> ); } export default function Demo() { return ( <div className="w-full max-w-xl"> <ImageCompare before={<Wireframe />} after={<Final />} beforeLabel="Wireframe" afterLabel="Final" /> </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.