react-carousel-zoom
v1.2.4
Published
Responsive image/video carousel with tap-to-zoom, swipe, thumbnails, and analytics hooks
Maintainers
Readme
react-carousel-zoom
Responsive React image/video carousel with tap-to-zoom, swipe navigation, thumbnails, video support, optional fullscreen overlay, and analytics hooks.
Features
- Images & video — mixed slides with posters, autoplay rules, and resume playback on slide return
- Thumbnails — square thumb strip with auto-scroll; video slides show a play badge
- Swipe & navigation — touch/pointer swipe, prev/next buttons, keyboard arrows
- Tap-to-zoom — zoom at tap point (2x/3x), pan when zoomed, second tap resets
- Fullscreen overlay — optional first-tap fullscreen on images (separate desktop/mobile control)
- Sizing — fixed height, aspect ratio, or adaptive height per active slide
- Responsive images — native
srcSet/sizes; lazy-load off-screen slides - Auto slide — timed advance with pause on hover, zoom, drag, or fullscreen
- Analytics — callback for taps, thumbs, nav, swipe, zoom, auto-slide, and fullscreen
- Accessible — keyboard nav, tab panels, live region announcements
Install
npm install react-carousel-zoomPeer dependencies: react and react-dom (v17+).
Styles (required):
import 'react-carousel-zoom/styles.css';Next.js App Router: the package ships with a "use client" banner — import CarouselZoom in a Client Component.
Quick start
import { CarouselZoom, type MediaItem } from 'react-carousel-zoom';
import 'react-carousel-zoom/styles.css';
const items: MediaItem[] = [
{
id: '1',
type: 'image',
src: '/images/product-large.jpg',
srcSet: '/images/product-400.jpg 400w, /images/product-800.jpg 800w',
sizes: '(max-width: 768px) 100vw, 50vw',
alt: 'Product front view',
},
{
id: '2',
type: 'video',
src: '/images/video-poster.jpg',
poster: '/images/video-poster.jpg',
videoSrc: '/videos/product.mp4',
aspectRatio: 16 / 9,
},
];
export function ProductGallery() {
return (
<CarouselZoom
items={items}
loop
mediaFit="cover"
aspectRatio={1}
zoomScale={2}
onAnalytics={(event) => console.log(event.type, event.item.id)}
/>
);
}Product page (PDP) example
Typical setup: cover inline, fullscreen on mobile tap, white fullscreen background, contain in fullscreen (automatic).
<CarouselZoom
items={items}
activeIndex={index}
onIndexChange={setIndex}
mediaFit="cover"
aspectRatio={1}
size={{ width: '100%' }}
fullscreenOnImageClick={{ desktop: false, mobile: true }}
mobileMaxWidth={768}
fullscreenBackground="#ffffff"
showThumbnails
showNavigation
loop
/>Exports
| Export | Description |
|--------|-------------|
| CarouselZoom | Main component |
| DEFAULT_MOBILE_MAX_WIDTH | Default mobile breakpoint (768) |
| MediaItem, CarouselZoomProps, FullscreenOnImageClick, … | TypeScript types |
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| items | MediaItem[] | required | Ordered slides (images and videos) |
| showThumbnails | boolean | true | Thumbnail strip below carousel |
| showNavigation | boolean | true | Prev/next arrow buttons |
| loop | boolean | false | Infinite loop between first and last slide |
| size | CarouselSize | — | Viewport width / height (e.g. { height: 400 }) |
| adaptiveHeight | boolean | false | Resize viewport to active slide aspect ratio |
| aspectRatio | number | 4/3 | Fixed viewport ratio when height is not set |
| zoomScale | number | 2 | Zoom multiplier (clamped to ≥ 1) |
| zoomOnHover | boolean | false | Zoom on mouse hover on desktop (tap-to-zoom disabled while enabled) |
| defaultIndex | number | 0 | Initial slide (uncontrolled mode) |
| activeIndex | number | — | Controlled active slide index |
| onIndexChange | (index) => void | — | Called when the active slide changes |
| onAnalytics | (event) => void | — | Analytics / tracking callback |
| thumbnailSize | number | 64 | Thumbnail size in px |
| swipeThreshold | number | 50 | Min horizontal swipe distance in px |
| autoSlide | boolean | false | Automatically advance slides |
| autoSlideInterval | number | 4000 | Auto-slide interval in ms |
| pauseAutoSlideOnHover | boolean | true | Pause auto-slide while hovered |
| videoAutoPlay | boolean | true | Autoplay video on user navigation only |
| videoMuted | boolean | true | Mute video (helps autoplay) |
| videoLoop | boolean | false | Loop video while slide is active |
| videoControls | boolean | true | Show native video controls |
| mediaFit | 'cover' \| 'contain' | 'cover' | How media fills the inline slide |
| className | string | — | Root class name (inline mode only) |
| ariaLabel | string | 'Image and video carousel' | Accessible region label |
| fullscreenOnImageClick | boolean \| { desktop?, mobile? } | off | First image tap opens fullscreen overlay |
| mobileMaxWidth | number | 768 | Viewport width (px) at or below = mobile |
| fullscreenBackground | string | #000 | Fullscreen overlay background color |
MediaItem
| Field | Type | Description |
|-------|------|-------------|
| id | string | Unique identifier |
| type | 'image' \| 'video' | Slide type |
| src | string | Image URL (fallback / video poster) |
| srcSet | string | Responsive srcset for images |
| sizes | string | sizes attribute for images |
| alt | string | Alt text |
| videoSrc | string | Video URL (required for type: 'video') |
| poster | string | Video poster frame |
| videoLoop | boolean | Per-slide loop override |
| videoControls | boolean | Per-slide controls override |
| thumbnailSrc | string | Custom thumbnail image |
| aspectRatio | number | Width÷height before media loads (e.g. 16/9) |
Slide order always follows the items array.
Sizing
Viewport size is resolved in this order:
size.heightset — fixed height;aspectRatiois ignoredadaptiveHeight— height follows the active slide’s aspect ratioaspectRatio— fixed ratio (e.g.1= square,16/9= widescreen)
Do not pass size.height if you want aspectRatio to control the shape:
<CarouselZoom items={items} aspectRatio={1} size={{ width: '100%' }} />Zoom
- First tap on an image zooms to
zoomScale, centered on the tap point - While zoomed, drag to pan within bounds
- Second tap resets to 1×
On desktop, set zoomOnHover={true} to zoom while the cursor is over the image (the view follows the cursor). Zoom resets on mouse leave. Tap-to-zoom is disabled while hover zoom is enabled.
Swipe is disabled while zoomed. In fullscreen, thumbnails, arrows, and the close button hide while zoomed.
Fullscreen overlay
Enable per platform:
// Mobile only (common PDP pattern)
fullscreenOnImageClick={{ desktop: false, mobile: true }}
// Both platforms
fullscreenOnImageClick={true}| Behavior | Detail |
|----------|--------|
| Trigger | First tap on a main image (not video) when enabled for current device |
| Overlay | Portaled to document.body, edge-to-edge, max z-index |
| mediaFit | Always contain in fullscreen; inline uses your mediaFit prop |
| Background | fullscreenBackground prop (default #000) |
| Close | X button, Escape, or browser back |
| Zoom in fullscreen | Tap-to-zoom works; chrome hides while zoomed |
Mobile vs desktop is determined by mobileMaxWidth (default 768px).
Video
- Autoplay when reached via user navigation (thumbs, arrows, swipe) if
videoAutoPlayis true - Auto-slide does not trigger video autoplay
- Playback position is preserved when leaving and returning to a slide
- When
videoControlsis false, tap the video area to play/pause - Missing
videoSrcshows a fallback UI (dev console warning in development)
Analytics events
| type | When fired |
|--------|------------|
| main_click | Tap on main image |
| thumb_click | Thumbnail selected |
| nav_prev / nav_next | Arrow button used |
| swipe | Horizontal swipe (meta.direction: 'left' | 'right') |
| zoom_in / zoom_out | Image zoom toggled |
| auto_slide | Slide advanced by timer |
| fullscreen_open / fullscreen_close | Fullscreen overlay opened or closed |
Accessibility
- Keyboard: focus viewport → ←/→ change slides, Home/End jump to ends
- Tabs: thumbnails linked to slide panels via
aria-controls - Live region: slide changes are announced
Development
git clone <your-repo>
cd react-carousel-zoom
npm install
npm run dev # demo at http://localhost:5173
npm run build # build dist/ for npm
npm run typecheckOnly dist/ and LICENSE are published to npm. The demo/ app is for local development.
Publish
npm login
npm run build
npm publishLicense
MIT © Om Vaishnav
