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 started
WireframeFinal

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/image-compare.tsx

    components/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>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.