Data Display · @parsakafi/astro-ui · BeforeAfterNew

BeforeAfterJS

Draggable image comparison — two images, one split, native range control.

BeforeAfter stacks two images in one frame and clips the “before” layer at the split position, revealing the “after” layer underneath. The split is driven by a real <input type="range"> stretched across the frame, so pointer drag, touch, and keyboard (Arrow keys, Home, End, Page Up/Down) work natively — the scoped script only mirrors the input value into a CSS custom property. value sets the initial position, aspectRatio shapes the frame, and labels adds Before/After chips. Clipping uses logical properties plus :dir() so the reveal starts on the reading side in RTL. Every move emits a bubbling before-after:change event with detail.value (0–100).

Features

  • Native range input drives the split — pointer, touch, and keyboard all work
  • Independent src + alt for each image
  • Initial split via value (0–100)
  • Any CSS aspect-ratio for the frame
  • Optional Before/After chips (labels)
  • RTL-aware reveal via logical properties and :dir()
  • before-after:change event carries the current percentage
  • Token-driven line, handle, shadow, and focus ring

Usage

---
import { BeforeAfter } from "@parsakafi/astro-ui";
---

<BeforeAfter
  before={{ src: "/before.jpg", alt: "Empty room" }}
  after={{ src: "/after.jpg", alt: "Furnished room" }}
/>

Demos

Basic

Sunny mountain lakeOvercast mountain lake
<BeforeAfter
  before={{ src: "/demo/before-after/before.svg", alt: "Overcast mountain lake" }}
  after={{ src: "/demo/before-after/after.svg", alt: "Sunny mountain lake" }}
/>

With labels

Sunny mountain lakeOvercast mountain lakeBeforeAfter

labels renders a chip per image; value=35 starts the split at 35%. Drag the handle, click anywhere on the frame, or focus the range and use the arrow keys.

<BeforeAfter
  labels
  value={35}
  before={{ src: "/demo/before-after/before.svg", alt: "Overcast mountain lake" }}
  after={{ src: "/demo/before-after/after.svg", alt: "Sunny mountain lake" }}
/>

Custom ratio and start position

Sunny mountain lakeOvercast mountain lake

aspectRatio accepts any CSS aspect-ratio value; the images crop with object-fit: cover.

<BeforeAfter
  aspectRatio="4 / 3"
  value={70}
  before={{ src: "/demo/before-after/before.svg", alt: "Overcast mountain lake" }}
  after={{ src: "/demo/before-after/after.svg", alt: "Sunny mountain lake" }}
/>

Cinematic crop

Final renderDraft renderDraftFinal

beforeLabel / afterLabel rename the chips; ariaLabel names the whole region. Listen for before-after:change to sync external UI.

<BeforeAfter
  aspectRatio="21 / 9"
  labels
  beforeLabel="Draft"
  afterLabel="Final"
  ariaLabel="Draft versus final render"
  before={{ src: "/demo/before-after/before.svg", alt: "Draft render" }}
  after={{ src: "/demo/before-after/after.svg", alt: "Final render" }}
/>

API

Props

PropTypeDefaultDescription
before{ src: string; alt: string }—Image revealed on the start side of the split.
after{ src: string; alt: string }—Image revealed on the end side of the split.
valuenumber50Initial split position, 0–100.
aspectRatiostring"16 / 9"Frame aspect ratio (any CSS aspect-ratio value).
ariaLabelstring"Before and after comparison"Accessible name for the comparison region.
rangeLabelstring"Comparison position"Accessible name for the range control.
labelsbooleanfalseShow Before/After chips over the images.
beforeLabelstring"Before"Text of the start-side chip.
afterLabelstring"After"Text of the end-side chip.
classstring—Merged via cn(); Tailwind classes welcome.
idstring—Element id.
ariaLabelstring—Accessible name.

Accessibility

Implemented accessibility features — semantics, ARIA wiring, and keyboard behavior that ship with the component.

  • Root is role=group named by ariaLabel
  • The split control is a native input[type=range], so screen readers announce a percentage and keyboard stepping works
  • aria-valuetext mirrors the current position
  • The decorative divider and handle are aria-hidden; both images keep their alt text
  • Focus is shown on the handle with :focus-visible and the shared ring tokens

FAQ

Why a range input instead of a custom drag handler?

The browser already ships the interaction: pointer capture, touch, keyboard stepping, Home/End, and screen-reader value announcements. The component only maps the input value to a clip position, so there is no drag math to get wrong.

How do I react when the split moves?

Listen for before-after:change on the root (it bubbles) and read event.detail.value, or listen for the native input event on the range inside. The event fires on every movement, including keyboard changes.

Does it work without JavaScript?

The initial split renders at value and both images are present, but dragging needs the scoped script. Treat it as a JavaScript-required component and provide a static image pair as the no-JS fallback.

Changelog

Version history mentioning BeforeAfter.

v0.1.0 — 2026-11-11
added
  • Data display (11): Badge, Avatar, Attachment, BeforeAfter, Table, Progress, Bubble, Chart, Empty, Tag, Timeline.