Interactive before/after image comparison slider with zoom, labels, and accessibility. Zero dependencies.
Live Demo | Vanilla Sandbox | React Sandbox
Existing before/after sliders are often rigid, inaccessible, or missing features like zoom and vertical orientation. This library was built to fill the gap:
- Lightweight — under 15 KB gzipped with zero runtime dependencies
- Accessible by default — WCAG 2.1 AA compliant out of the box
- Framework-agnostic — works with vanilla JS, React, or any framework
- Built-in zoom & pan — no need for a separate zoom library
- Multiple interaction modes — drag, hover, or click
- Optional Cloudimage CDN — serve optimally-sized images automatically
- Three interaction modes — Drag, hover, or click to reveal before/after
- Horizontal & vertical — Slider works in both orientations
- Zoom & Pan — CSS transform-based with mouse wheel, pinch-to-zoom, double-click, drag-to-pan
- Handle styles — Arrows, circle, or minimal line
- Labels — Customizable before/after labels with configurable position
- Entrance animation — Smooth slider animation on first view with configurable duration, delay, and easing
- Fullscreen mode — Built-in fullscreen toggle
- WCAG 2.1 AA — Full keyboard navigation, ARIA attributes, focus management, reduced motion
- CSS variable theming — Light and dark themes, fully customizable
- Two init methods — JavaScript API and HTML data-attributes
- React wrapper — Separate entry point with component, hook, and ref API
- TypeScript — Full type definitions
- Cloudimage CDN — Optional responsive image loading
- Lazy loading — IntersectionObserver-based image lazy loading
npm install js-cloudimage-before-after<script src="https://scaleflex.cloudimg.io/v7/plugins/js-cloudimage-before-after/1.0.3/js-cloudimage-before-after.min.js?vh=b67ce8&func=proxy"></script>import CIBeforeAfter from 'js-cloudimage-before-after';
const viewer = new CIBeforeAfter('#slider', {
beforeSrc: 'https://example.com/kitchen-before.jpg',
afterSrc: 'https://example.com/kitchen-after.jpg',
beforeAlt: 'Kitchen before renovation',
afterAlt: 'Kitchen after renovation',
zoom: true,
labels: { before: 'Before', after: 'After' },
theme: 'light',
handleStyle: 'arrows',
animate: { duration: 800 },
onSlide(position) {
console.log('Position:', position);
},
});<div
data-ci-before-after-before-src="https://example.com/before.jpg"
data-ci-before-after-after-src="https://example.com/after.jpg"
data-ci-before-after-before-alt="Before renovation"
data-ci-before-after-after-alt="After renovation"
data-ci-before-after-zoom="true"
data-ci-before-after-theme="light"
data-ci-before-after-handle-style="arrows"
data-ci-before-after-label-before="Before"
data-ci-before-after-label-after="After"
></div>
<script>CIBeforeAfter.autoInit();</script>new CIBeforeAfter(element: HTMLElement | string, config: CIBeforeAfterConfig)| Option | Type | Default | Description |
|---|---|---|---|
beforeSrc |
string |
— | Before image URL (required) |
afterSrc |
string |
— | After image URL (required) |
beforeAlt |
string |
'Before' |
Before image alt text |
afterAlt |
string |
'After' |
After image alt text |
mode |
'drag' | 'hover' | 'click' |
'drag' |
Interaction mode |
orientation |
'horizontal' | 'vertical' |
'horizontal' |
Slider direction |
initialPosition |
number |
50 |
Starting position (0–100) |
handleStyle |
'arrows' | 'circle' | 'line' |
'arrows' |
Handle visual style |
labels |
boolean | { before?: string; after?: string } |
true |
Show labels or custom text |
labelPosition |
'top' | 'bottom' |
'top' |
Label placement |
theme |
'light' | 'dark' |
'light' |
Color theme |
zoom |
boolean |
false |
Enable zoom & pan |
zoomMax |
number |
4 |
Maximum zoom level |
zoomMin |
number |
1 |
Minimum zoom level |
zoomControls |
boolean |
true |
Show zoom control buttons |
zoomControlsPosition |
string |
'bottom-right' |
Zoom controls position (top-left, top-center, top-right, bottom-left, bottom-center, bottom-right) |
scrollHint |
boolean |
true |
Show scroll hint when zoomed |
animate |
boolean | AnimateConfig |
false |
Entrance animation |
animateOnce |
boolean |
true |
Animate only on first view |
fullscreenButton |
boolean |
true |
Show fullscreen button |
lazyLoad |
boolean |
true |
Lazy load images |
keyboardStep |
number |
1 |
Arrow key step (%) |
keyboardLargeStep |
number |
10 |
Shift+Arrow step (%) |
onSlide |
(position: number) => void |
— | Position change callback |
onZoom |
(level: number) => void |
— | Zoom change callback |
onFullscreenChange |
(isFullscreen: boolean) => void |
— | Fullscreen callback |
onReady |
() => void |
— | Ready callback |
cloudimage |
CloudimageConfig |
— | Cloudimage CDN config |
| Option | Type | Default | Description |
|---|---|---|---|
duration |
number |
800 |
Duration in ms |
delay |
number |
0 |
Delay before start in ms |
easing |
string |
'ease-out' |
CSS easing function |
instance.setPosition(percent: number): void // Set slider position (0–100)
instance.getPosition(): number // Get current position
instance.setZoom(level: number): void // Set zoom level
instance.getZoom(): number // Get current zoom level
instance.resetZoom(): void // Reset zoom to 1×
instance.enterFullscreen(): void // Enter fullscreen
instance.exitFullscreen(): void // Exit fullscreen
instance.isFullscreen(): boolean // Check fullscreen state
instance.update(config: Partial<Config>): void // Update config
instance.destroy(): void // Destroy instance
instance.getElements(): Elements // Get DOM elementsCIBeforeAfter.autoInit(root?: HTMLElement): CIBeforeAfterInstance[]import { CIBeforeAfterViewer, useCIBeforeAfter } from 'js-cloudimage-before-after/react';
// Component
function ImageComparison() {
return (
<CIBeforeAfterViewer
beforeSrc="/kitchen-before.jpg"
afterSrc="/kitchen-after.jpg"
beforeAlt="Kitchen before renovation"
afterAlt="Kitchen after renovation"
zoom
labels={{ before: 'Before', after: 'After' }}
handleStyle="arrows"
animate={{ duration: 800 }}
onSlide={(pos) => console.log('Position:', pos)}
/>
);
}
// Hook
function ImageComparison() {
const { containerRef, instance } = useCIBeforeAfter({
beforeSrc: '/kitchen-before.jpg',
afterSrc: '/kitchen-after.jpg',
zoom: true,
});
return (
<>
<div ref={containerRef} />
<button onClick={() => instance.current?.setZoom(2)}>Zoom 2×</button>
</>
);
}
// Ref API
function ImageComparison() {
const ref = useRef<CIBeforeAfterViewerRef>(null);
return (
<>
<CIBeforeAfterViewer
ref={ref}
beforeSrc="/kitchen-before.jpg"
afterSrc="/kitchen-after.jpg"
zoom
/>
<button onClick={() => ref.current?.setPosition(75)}>Show 75%</button>
</>
);
}All visuals are customizable via CSS variables:
.my-viewer {
--ci-before-after-border-radius: 12px;
--ci-before-after-handle-color: #0058a3;
--ci-before-after-handle-size: 44px;
--ci-before-after-handle-border: 2px solid white;
--ci-before-after-label-bg: rgba(0, 0, 0, 0.6);
--ci-before-after-label-color: #fff;
--ci-before-after-label-radius: 4px;
}Set theme: 'dark' for the built-in dark theme.
- Slider handle is a focusable element with
role="slider"andaria-valuenow Arrow keysmove the slider positionShift + Arrowmoves in larger stepsHome/Endjump to 0% / 100%+/-/0control zoomEscapeexits fullscreenprefers-reduced-motiondisables animations- Before/after images have configurable alt text
new CIBeforeAfter('#el', {
beforeSrc: 'https://example.com/before.jpg',
afterSrc: 'https://example.com/after.jpg',
cloudimage: {
token: 'demo',
limitFactor: 100,
params: 'q=80',
},
});| Browser | Version |
|---|---|
| Chrome | 80+ |
| Firefox | 80+ |
| Safari | 14+ |
| Edge | 80+ |
If this library helped your project, consider buying me a coffee!
Made with care by the Scaleflex team
