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
<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
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
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
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
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
value | number | 50 | Initial split position, 0–100. |
aspectRatio | string | "16 / 9" | Frame aspect ratio (any CSS aspect-ratio value). |
ariaLabel | string | "Before and after comparison" | Accessible name for the comparison region. |
rangeLabel | string | "Comparison position" | Accessible name for the range control. |
labels | boolean | false | Show Before/After chips over the images. |
beforeLabel | string | "Before" | Text of the start-side chip. |
afterLabel | string | "After" | Text of the end-side chip. |
class | string | — | Merged via cn(); Tailwind classes welcome. |
id | string | — | Element id. |
ariaLabel | string | — | 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-valuetextmirrors 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-visibleand 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.
- Data display (11): Badge, Avatar, Attachment, BeforeAfter, Table, Progress, Bubble, Chart, Empty, Tag, Timeline.