Loupe Compare

Wheat field with cypresses, restored to full colorWheat field with cypresses, as an archive print
About this component

Loupe Compare

A before and after comparison: two pictures stacked in one frame, the top one revealed up to a divider with a round handle. Drag the divider, click anywhere to send it there on a spring, or switch to hover mode so a mouse moves it without pressing; it runs sideways or top to bottom. The reveal is a clip-path and the position is a single CSS variable, so nothing reflows and moving costs no renders. Captions on each side fade as the divider reaches them. Both pictures load before either shows, then fade in together over a soft placeholder. The frame keeps its aspect ratio, and the keyboard works it like a slider: arrows, Page Up and Page Down, Home and End.

Usage

The import and the props worth knowing about, in one place.

import { LoupeCompare } from "@/components/beste/component/loupe-compare";

<LoupeCompare
  before={{ src: "/photos/raw.jpg", alt: "The stage before the lighting cue" }}
  after={{ src: "/photos/graded.jpg", alt: "The stage with the lighting cue" }}
  labels={{ before: "Raw", after: "Graded" }}
  aspectRatio="16/9"
/>

// Top to bottom, following the mouse without a press
<LoupeCompare
  before={{ src: "/photos/scan.jpg", alt: "Archive scan", className: "grayscale" }}
  after={{ src: "/photos/scan.jpg", alt: "Restored scan" }}
  orientation="vertical"
  mode="hover"
  onValueCommit={(value) => console.log("settled at", value)}
/>

Keyboard and gestures

Every one of these is in the component already. They are listed because a props table cannot mention a gesture, so nothing else on this page can tell you they exist.

DragMove the divider; it follows the pointer exactly.
ClickSend the divider to that spot on a spring.
Hover modeA mouse moves the divider without pressing.
Arrow keysMove it by 1%. On a vertical divider, up moves it up.
Page Up / Page DownMove it by 10%.
Home / EndSend it to either edge.

Props

Read from the component's own type, so this cannot drift from what it accepts.

PropTypeDefaultDescription
before*CompareImage—Shown on the start side of the divider: left, or top when vertical.
after*CompareImage—Shown on the end side of the divider.
valuenumber—Divider position from the start edge, 0 to 100. Pair with `onValueChange` to control it.
defaultValuenumber50
onValueChange(value: number) => void—Every move of the divider.
onValueCommit(value: number) => void—Once per gesture: on release, after a click or a key press.
orientation"horizontal" | "vertical""horizontal"
mode"drag" | "hover"—`drag` moves the divider on press; `hover` lets a mouse move it without pressing. @defaultValue "drag"
aspectRationumber | string—Width to height, as a number or a CSS ratio like "3/2". @defaultValue "3/2"
labels{ before?: string; after?: string } | false—Captions on each side; `false` hides them.
tone"muted" | "outline" | "ghost""muted"
size"sm" | "default" | "lg""default"
disabledboolean—
aria-labelstring"Compare before and after"
classNamestring—

More Loupe components

View all Loupe
Press Enter to magnify, then use the arrow keys to move, plus or minus to change the zoom and Escape to close.