Vize

Image Compare

Headless before/after image comparison with a WAI-ARIA slider handle.

Package @vizejs/ui/image-compare
Maturity stable
Own the source vize lib pull image-compare
Requires context, controllable-state, id
Aliases image compare, before after, comparison slider, image diff, reveal slider
Covers WAI-ARIA Slider, img-comparison-slider, react-compare-slider, Ark UI Splitter-style comparisons

Usage

import { ImageCompareAfter, ImageCompareBefore, ImageCompareHandle, ImageCompareLabel, ImageCompare, ImageCompareRoot } from "@vizejs/ui/image-compare";

Or copy the source into your project with vize lib pull image-compare (see Source Distribution).

API

ImageCompareAfter

Source: src/families/media/image-compare/image-compare-after.vue

Slots

Slot Slot props Description
default ImageCompareSlotState The after image or content. Receives the divider state.

Exposed

Member Type Description
element Readonly<ShallowRef<HTMLElement | null>> Template ref to the rendered root element.

ImageCompareBefore

Source: src/families/media/image-compare/image-compare-before.vue

Slots

Slot Slot props Description
default ImageCompareSlotState The before image or content. Receives the divider state.

Exposed

Member Type Description
element Readonly<ShallowRef<HTMLElement | null>> Template ref to the rendered root element.

ImageCompareHandle

Source: src/families/media/image-compare/image-compare-handle.vue

Props

Prop Type Default Description
ariaLabel string undefined Accessible name. Defaults to messages.handleLabel, then "Comparison position".
ariaLabelledby string undefined Space-separated ids that label the handle; takes precedence over ariaLabel.
ariaDescribedby string undefined Space-separated ids that describe the handle.

Slots

Slot Slot props Description
default ImageCompareSlotState Handle contents such as a grip icon. Receives the divider state.

Exposed

Member Type Description
element HTMLDivElement | null Rendered slider element.

ImageCompareLabel

Source: src/families/media/image-compare/image-compare-label.vue

Props

Prop Type Default Description
side (required) ImageCompareSide required Which image this caption describes.

Slots

Slot Slot props Description
default ImageCompareSlotState Caption text such as "Before" or "After". Receives the divider state.

Exposed

Member Type Description
element Readonly<ShallowRef<HTMLElement | null>> Template ref to the rendered root element.

ImageCompare

Source: src/families/media/image-compare/image-compare-root.vue

Props

Prop Type Default Description
id string | null undefined Consumer-owned base id. null and undefined select a deterministic fallback.
modelValue number undefined Controlled divider position in percent (0–100). undefined selects uncontrolled behavior.
defaultValue number 50 Initial uncontrolled divider position in percent.
orientation ImageCompareOrientation "horizontal" Divider axis. horizontal moves a vertical divider left and right.
dir ImageCompareDirection "ltr" Reading direction; RTL measures horizontal positions from the right edge.
step number 1 Arrow-key increment and snapping step in percent. 0 disables snapping.
pageStep number 10 PageUp/PageDown increment in percent.
mode ImageCompareMode "drag" Pointer behavior: press-and-drag, or follow the hovering pointer.
disabled boolean false Suppress pointer and keyboard changes and remove the handle from the tab order.
messages ImageCompareMessages undefined Localized handle name and value text. Omitted entries use English defaults.

Events

Event Payload Description
update:modelValue [position: number] Fired with the requested position whenever it changes.
change [position: number, previous: number, source: ImageCompareChangeSource] Fired after every distinct position request with its source.
commit [position: number] Fired when a pointer drag or keyboard interaction finishes.

Slots

Slot Slot props Description
default ImageCompareSlotState Before/after images, handle, and labels. Receives the divider state.

Exposed

Member Type Description
element Readonly<ShallowRef<HTMLElement | null>> Template ref to the rendered root element.
focus —
orientation —
position —
setPosition —
state —

ImageCompareRoot

Source: src/families/media/image-compare/image-compare-root.vue

Props

Prop Type Default Description
id string | null undefined Consumer-owned base id. null and undefined select a deterministic fallback.
modelValue number undefined Controlled divider position in percent (0–100). undefined selects uncontrolled behavior.
defaultValue number 50 Initial uncontrolled divider position in percent.
orientation ImageCompareOrientation "horizontal" Divider axis. horizontal moves a vertical divider left and right.
dir ImageCompareDirection "ltr" Reading direction; RTL measures horizontal positions from the right edge.
step number 1 Arrow-key increment and snapping step in percent. 0 disables snapping.
pageStep number 10 PageUp/PageDown increment in percent.
mode ImageCompareMode "drag" Pointer behavior: press-and-drag, or follow the hovering pointer.
disabled boolean false Suppress pointer and keyboard changes and remove the handle from the tab order.
messages ImageCompareMessages undefined Localized handle name and value text. Omitted entries use English defaults.

Events

Event Payload Description
update:modelValue [position: number] Fired with the requested position whenever it changes.
change [position: number, previous: number, source: ImageCompareChangeSource] Fired after every distinct position request with its source.
commit [position: number] Fired when a pointer drag or keyboard interaction finishes.

Slots

Slot Slot props Description
default ImageCompareSlotState Before/after images, handle, and labels. Receives the divider state.

Exposed

Member Type Description
position number Divider position from 0 (all after) to 100 (all before), in percent.
orientation ImageCompareOrientation Divider axis.
state ImageCompareState Interaction state.
element HTMLDivElement | null Rendered root element.
setPosition (position: number) => boolean Request a position (clamped and snapped). Reports whether it changed.
focus (options?: FocusOptions) => void Move focus to the handle.

Behavior

Normative state x input -> outcome table for image-compare-root.vue, image-compare-before.vue, image-compare-after.vue, image-compare-label.vue, and image-compare-handle.vue (@vizejs/ui/image-compare). Every row is proven by the named test.

The root owns the divider position (0–100, percent) and publishes it as --vize-ui-image-compare-position. No CSS ships; a typical recipe stacks both images and clips the "before" side:

[data-vize-ui="image-compare-root"] {
  position: relative;
}
[data-vize-ui="image-compare-before"],
[data-vize-ui="image-compare-after"] {
  position: absolute;
  inset: 0;
}
[data-vize-ui="image-compare-before"] {
  z-index: 1;
  clip-path: inset(0 calc(100% - var(--vize-ui-image-compare-position)) 0 0);
}
[data-vize-ui="image-compare-handle"] {
  position: absolute;
  inset-block: 0;
  left: var(--vize-ui-image-compare-position);
}

The handle follows the WAI-ARIA slider pattern. Arrow keys move the divider in the pressed direction: Right/Left follow the reading direction and Up/Down move a vertical divider up and down (its value is measured from the top edge).

ID State Input Outcome Evidence
IC1 default render root publishes the position variable and data hooks; the handle is a focusable slider with min/max/now/valuetext and orientation renders slider semantics, position custom property, parts, and slot state
IC2 focused handle Arrow / Page / Home / End moves by step, pageStep, or to an end; clamps at 0 and 100; other keys pass through; emits change (keyboard) and commit keyboard follows the slider pattern with step, page step, Home, and End
IC3 RTL / vertical keys and pointer RTL measures from the right edge and mirrors horizontal arrows; vertical measures from the top and maps Up/Down right-to-left and vertical dividers map keys and pointers to the visual direction
IC4 drag mode primary pointer down / move / up captures the pointer, focuses the handle, tracks only that pointer, marks dragging, and emits commit on release; other buttons ignored pointer drags move the divider with capture, focus the handle, and commit on release
IC5 hover mode mouse move / touch the divider follows hovering mouse and pen pointers without pressing; touch and presses never drag hover mode follows mouse pointers without pressing and ignores touch
IC6 unmeasured root pointer down zero-size roots ignore pointers unmeasured roots ignore pointers
IC7 controlled key press / prop change emits the requested position while the controlled value wins; out-of-range values clamp controlled position wins until the parent accepts the request
IC8 disabled keys / pointer / API the handle leaves the tab order with aria-disabled; user input is ignored; setPosition() still applies disabled roots leave the tab order and ignore keys and pointers
IC9 messages / names render handle name and value text come from typed messages; ariaLabel / ariaLabelledby / ariaDescribedby override messages and explicit names localize the handle
IC10 exposed instance read / setPosition() / focus() exposes position, orientation, state, element; requests snap to step and report whether they changed exposes typed state and imperative position and focus controls
IC11 missing provider setup compound parts fail closed with the shared context diagnostic compound parts require a matching root provider
IC12 pure helpers normalize / pointer / key clamping, snapping, pointer mapping per axis and direction, and slider key mapping are deterministic image-compare-value.test.ts
IC13 SSR isolated requests markup is byte-identical, with the position variable and slider attributes rendered on the server renders byte-identical image compare markup across isolated SSR requests
IC14 SSR / hydration hydrate server markup hydrates without warnings or node replacement hydrates image compare markup without warnings or node replacement
IC15 types compile unions, slot state, messages, and exposes are closed and read-only image-compare.types.test-d.ts

Pointer listeners attach on mount, so server markup carries no handlers.