motion-image-compare

Before/after slider with a draggable, spring-damped handle. Drop two images into the before and after slots. Drag, click, or use the arrow keys to reveal.

Preview: Horizontal

Before
After

Preview: Vertical

Top
Bottom

Import

import 'motion-components/motion-image-compare'   // just this one
import 'motion-components'               // or the whole library

Usage

<motion-image-compare start="50">
  <img slot="before" src="before.jpg" alt="Before retouch" />
  <img slot="after"  src="after.jpg"  alt="After retouch"  />
</motion-image-compare>

Sizing

The host fills its container's width and clips overflow. Give it an explicit height (or use an aspect-ratio container). Slotted images stretch to 100% × 100% with object-fit: cover.

motion-image-compare {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 12px;
}

Properties

Attribute Property Type Default Description
start start number 50 Initial split position as a percentage (0–100)
orientation orientation "horizontal" | "vertical" "horizontal" Drag axis
bounce bounce number 0.25 Spring bounciness of the snap-to-cursor animation
duration duration number 0.45 Spring duration of the snap-to-cursor animation, in seconds

Slots

Slot Type Default Description
before element — The left/top image (visible behind the clip)
after element — The right/bottom image (revealed by dragging)

CSS custom properties

Property Type Default Description
--mc-color-accent color #2563eb Color of the keyboard focus ring on the knob

JavaScript properties

Property Description
position Split position as a percentage (0 to 100). Reading it gives where the split is going. Setting it springs the split there without firing motion-change. Set before the first render, it sets the position the split starts at.

Events

Event Detail Description
motion-change position Fired when a drag, click or key moves the split. position is the percentage it moves to. Not fired when you set position. Bubbles and is composed.

Other components also fire a bubbling motion-change. A listener on a parent element should check event.target.

import type { MotionImageCompare } from 'motion-components'

const compare = document.querySelector<MotionImageCompare>('motion-image-compare')!

compare.position = 75 // springs to 75%, no event

compare.addEventListener('motion-change', (event) => {
  if (event.target !== compare) return
  console.log('split at', event.detail.position)
})

Accessibility

The handle is a focusable role="slider" labelled "Image comparison", with aria-valuemin/max/now. Use ← / → (or ↑ / ↓ in vertical mode) to nudge the split by 2%, hold Shift for 10% jumps. Home and End move it to 0% and 100%. Slotted images are kept as the source of truth; remember to write meaningful alt text.