compare-surface
Before/after image comparison slider with drag handle, keyboard support, and progressive enhancement.
Overview
The <compare-surface> component creates a before/after comparison slider. Two children overlay each other, and a draggable divider reveals one side. Works with any two children — images, divs, or any other elements.
<compare-surface> <img src="before.jpg" alt="Before" /> <img src="after.jpg" alt="After" /></compare-surface>
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
position |
number | 50 |
Initial slider position from 0 (fully left) to 100 (fully right). |
Custom Start Position
Set position to control where the divider starts. A value of 25 means the divider starts 25% from the left, showing mostly the “after” content.
<!-- Start at 25% --><compare-surface position="25"> <img src="before.jpg" alt="Before" /> <img src="after.jpg" alt="After" /></compare-surface>
Non-Image Content
The component works with any two child elements, not just images. Use divs, text blocks, or any HTML content.
Draft
The original version before revisions.
Final
The polished version with improved clarity.
<compare-surface> <div class="panel-before">Draft version</div> <div class="panel-after">Final version</div></compare-surface>
Keyboard Navigation
| Key | Action |
|---|---|
| ArrowLeft / ArrowDown | Move divider 1% left |
| ArrowRight / ArrowUp | Move divider 1% right |
| Shift + Arrow | Move divider 10% in the arrow direction |
Events
| Event | Detail | Description |
|---|---|---|
compare-surface:change |
{ position: number } |
Fired when the slider position changes (drag or keyboard). |
const slider = document.querySelector('compare-surface'); slider.addEventListener('compare-surface:change', (event) => { console.log('Position:', event.detail.position);});
Accessibility
Slider Role
The divider has role="slider" with aria-valuemin, aria-valuemax, and aria-valuenow. Screen readers announce position changes as the slider moves.
Keyboard Support
The divider is focusable (tabindex="0") and responds to arrow keys. Shift + arrow provides larger steps for faster navigation.
Touch Support
The divider uses touch-action: none and setPointerCapture for reliable touch/pointer drag behavior across devices.
Styling
The divider and handle can be styled with CSS. The divider is a .comparison-divider element with a ::after pseudo-element for the circular handle.
/* Custom divider color */.comparison-divider { background: var(--color-interactive);} /* Custom handle */.comparison-divider::after { background: var(--color-interactive); border-color: white;}
Progressive Enhancement
Without JavaScript, both children display side-by-side in a natural grid. The :not(:defined) selector provides the fallback layout. Once JS registers the component, children overlap and the clip-path slider takes over.
/* Without JS: side-by-side grid */compare-surface:not(:defined) { display: grid; grid-template-columns: 1fr 1fr; gap: var(--size-s);}
Related
<content-swap>— Two-face content toggle with transitions<tab-set>— Tab panels for content switching