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.