Comparison Table
A plan-versus-feature matrix built as a real table, with readable boolean cells.
6 frameworksBeginner
Two photographs stacked under a draggable wipe handle that also works from the keyboard.
<!--
Two photographs stacked, with the top one clipped to a movable edge. The edge
is the entire control - and a control you can only drag is a control a
keyboard user cannot touch. It is a real role="slider": arrow keys nudge,
Home/End jump to either extreme, and aria-valuenow reports where it is.
Both images keep their own alt text. They are two different pictures of the
same place, not one picture with a decoration on top.
-->
<div class="wipe" data-wipe>
<img class="wipe__image" src="/images/kitchen-after.jpg" alt="Kitchen after the renovation" />
<div class="wipe__clip" data-wipe-clip>
<img class="wipe__image" src="/images/kitchen-before.jpg" alt="Kitchen before the renovation" />
</div>
<span class="wipe__tag wipe__tag--before" aria-hidden="true">Before</span>
<span class="wipe__tag wipe__tag--after" aria-hidden="true">After</span>
<div
class="wipe__handle"
data-wipe-handle
role="slider"
tabindex="0"
aria-label="Reveal the before image"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="50"
aria-valuetext="50% before"
>
<span class="wipe__grip" aria-hidden="true"></span>
</div>
</div>
<script>
(function () {
document.querySelectorAll('[data-wipe]').forEach(function (root) {
var clip = root.querySelector('[data-wipe-clip]');
var handle = root.querySelector('[data-wipe-handle]');
var pos = 50;
function set(next) {
pos = Math.min(100, Math.max(0, next));
// inset() from the right: the before image keeps its left-hand slice.
clip.style.clipPath = 'inset(0 ' + (100 - pos) + '% 0 0)';
handle.style.left = pos + '%';
handle.setAttribute('aria-valuenow', String(Math.round(pos)));
handle.setAttribute('aria-valuetext', Math.round(pos) + '% before');
}
function fromPointer(event) {
var rect = root.getBoundingClientRect();
set(((event.clientX - rect.left) / rect.width) * 100);
}
// One pointer path covers mouse, touch and pen; the capture keeps the drag
// alive after the cursor leaves the frame, which a mousemove listener on
// the element alone would not.
root.addEventListener('pointerdown', function (event) {
root.setPointerCapture(event.pointerId);
fromPointer(event);
});
root.addEventListener('pointermove', function (event) {
if (root.hasPointerCapture(event.pointerId)) fromPointer(event);
});
handle.addEventListener('keydown', function (event) {
// Shift jumps in tens - 100 arrow presses to cross the image is not a
// keyboard equivalent of a drag, it is a punishment.
var step = event.shiftKey ? 10 : 1;
var next = null;
if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') next = pos - step;
else if (event.key === 'ArrowRight' || event.key === 'ArrowUp') next = pos + step;
else if (event.key === 'Home') next = 0;
else if (event.key === 'End') next = 100;
if (next === null) return;
event.preventDefault();
set(next);
});
set(pos);
});
})();
</script>| Prop | Type | Default | Description |
|---|---|---|---|
beforerequired | WipeImage | - | The image shown on the "before" side. |
afterrequired | WipeImage | - | The image shown on the "after" side. |
defaultValue | number | 50 | Where the divider sits, as a percentage from the left. |
ariaLabel | string | - | The button's accessible name. Required for icon-only buttons. |
className | string | - | Additional classes merged onto the root element. |
The wipe is a `clip-path: inset()` on the top layer rather than a width change, because both layers stay full size and therefore pixel-aligned - resize one instead and the two photographs slide against each other as the edge moves. The handle is the reason this component is rated advanced: it is a real `role="slider"` with `aria-valuenow`, arrow keys, Shift for ten-percent jumps and Home/End, because a handle you can only drag is a handle a keyboard user cannot touch at all, and `role="slider"` without key handling is a promise the component does not keep. Pointer Events (plus `setPointerCapture`) cover mouse, touch and pen in one path and keep the drag alive when the cursor leaves the frame; `touch-action: none` is what stops a touch drag scrolling the page instead. Give both images real, different alt text - they are two pictures, not one with a decoration on top.