Lightweight image carousel with zoom, swipe, transitions, and accessibility. Zero dependencies.
- Project Overview
- Core Features
- API Design
- Visual Design
- Zoom & Pan
- Transitions
- React Wrapper API
- Accessibility
- Build & Distribution
- Project Structure
- GitHub Pages Demo
- Additional Features
- Competitor Feature Matrix
- Roadmap
@cloudimage/carousel is an open-source JavaScript library for creating image carousels and image viewers. It provides a lightweight, accessible, and feature-rich carousel component with built-in zoom, swipe gestures, multiple transition effects, and optional Cloudimage CDN integration for responsive image delivery.
The existing ecosystem for image carousels has significant gaps:
- Most libraries are heavy and require jQuery or large framework dependencies
- Accessibility (WCAG compliance, keyboard navigation, screen readers) is poorly covered or absent
- Built-in zoom & pan is rarely combined with carousel navigation in a single package
- TypeScript support is often incomplete or nonexistent
- No modern library offers both vanilla JS and React with a proper build pipeline
- HTML data-attribute initialization for CDN/no-build usage is uncommon
- CSS variable theming for easy customization without JavaScript is rare
@cloudimage/carousel fills these gaps by providing:
- A zero-dependency, TypeScript-first library
- Combined zoom/pan + carousel in a single package
- Four transition effects — slide, fade, zoom, and flip
- Two equal initialization methods — JavaScript API and HTML data-attributes
- WCAG 2.1 AA accessibility compliance out of the box
- CSS variable theming for easy customization (30+ properties)
- A React wrapper with SSR support
- Modern build output — ESM, CJS, and UMD in a single package
- < 15 KB gzipped bundle size (JS + CSS)
- Scaleflex
cloudimage-hotspot— same build system pattern, React wrapper architecture, accessibility approach, deployment pipeline - Scaleflex
cloudimage-360— CDN integration pattern, UMD global naming convention
| Feature | Description |
|---|---|
| Image Carousel | Navigate between images with thumbnails, bullets, swipe gestures, or keyboard |
| Transitions | Four effects: slide, fade, zoom, and flip — configurable via CSS variables |
| Zoom & Pan | CSS transform-based, GPU-accelerated; Ctrl+scroll, double-click, pinch-to-zoom, drag-to-pan |
| Fullscreen | Browser Fullscreen API toggle with focus trapping |
| Autoplay | Configurable interval with pause/resume controls (WCAG 2.2.2 compliant) |
| Accessibility | WCAG 2.1 AA; full keyboard navigation; ARIA attributes; focus management; reduced motion |
| Theming | CSS variables as primary customization; light and dark built-in themes |
| Two Init Methods | JavaScript API (new CloudImageCarousel()) and HTML data-attributes (data-ci-carousel-*) — fully equivalent |
| React Wrapper | Separate entry point with component, hook, and ref API |
| TypeScript | Full type definitions, exported interfaces and types |
| Cloudimage Integration | Optional responsive image loading via Scaleflex Cloudimage CDN |
| Lazy Loading | IntersectionObserver-based with eager fallback |
| Swipe Gestures | Touch support for mobile navigation |
| Thumbnails | Configurable thumbnail strip with active state |
| Bullets | Indicator dots with aria-pressed for accessibility |
| Build Formats | ESM + CJS + UMD; single CDN file; window.CloudImageCarousel global |
The library provides two fully equivalent initialization methods. Every configuration option available in the JavaScript API is also expressible via HTML data-attributes.
const carousel = new CloudImageCarousel(element, config);
carousel.init();Parameters:
| Parameter | Type | Description |
|---|---|---|
element |
HTMLElement | string |
Container element or CSS selector |
config |
Partial<CloudImageCarouselConfig> |
Configuration object |
CloudImageCarouselConfig interface:
interface CloudImageCarouselConfig {
/** Image sources — strings or { src, alt } objects */
images?: ImageSource[];
/** Enable automatic slide advancement (default: false) */
autoplay?: boolean;
/** Autoplay interval in milliseconds, min 100 (default: 3000) */
autoplayInterval?: number;
/** Loop from last slide back to first (default: true) */
cycle?: boolean;
/** Show filename overlay on each slide (default: false) */
showFilenames?: boolean;
/** Show thumbnail strip below the carousel (default: true) */
showThumbnails?: boolean;
/** Show bullet indicators (default: false) */
showBullets?: boolean;
/** Show navigation controls — prev/next/fullscreen (default: true) */
showControls?: boolean;
/** Position of prev/next navigation arrows (default: 'center') */
controlsPosition?: 'center' | 'bottom';
/** Color theme (default: 'light') */
theme?: 'light' | 'dark';
/** Slide transition effect (default: 'fade') */
transitionEffect?: 'slide' | 'fade' | 'zoom' | 'flip';
/** Minimum zoom level (default: 1) */
zoomMin?: number;
/** Maximum zoom level (default: 4) */
zoomMax?: number;
/** Zoom step increment (default: 0.3) */
zoomStep?: number;
/** Callback fired after a slide change */
onSlideChange?: (index: number) => void;
/** Callback fired when an image fails to load */
onError?: (src: string, index: number) => void;
/** Optional Cloudimage CDN integration */
cloudimage?: CloudimageConfig;
}CloudimageConfig interface:
interface CloudimageConfig {
/** Cloudimage customer token (e.g. 'demo'). Enables Cloudimage when set. */
token: string;
/** API version (default: 'v7') */
apiVersion?: string;
/** Custom Cloudimage domain (default: 'cloudimg.io') */
domain?: string;
/** Round widths to the nearest N pixels (default: 100) */
limitFactor?: number;
/** Custom URL params appended to CDN URL (e.g. 'q=80&org_if_sml=1') */
params?: string;
}ImageSource type:
type ImageSource = string | { src: string; alt?: string };When a string is provided, alt text defaults to "Image N" (where N is the 1-based index).
Instance methods:
interface CloudImageCarouselInstance {
/** Initialize the carousel (build DOM, attach events) */
init(): void;
/** Navigate to the next slide */
next(): void;
/** Navigate to the previous slide */
prev(): void;
/** Navigate to a specific slide by index */
goToSlide(index: number): void;
/** Zoom in (centered) */
zoomIn(): void;
/** Zoom out (centered) */
zoomOut(): void;
/** Reset zoom to 1x */
resetZoom(): void;
/** Toggle browser fullscreen */
toggleFullscreen(): void;
/** Start autoplay */
startAutoplay(): void;
/** Stop autoplay */
stopAutoplay(): void;
/** Pause autoplay (can be resumed) */
pauseAutoplay(): void;
/** Resume paused autoplay */
resumeAutoplay(): void;
/** Switch theme at runtime */
setTheme(theme: 'light' | 'dark'): void;
/** Replace images after initialization */
loadImages(sources: ImageSource[]): void;
/** Tear down the carousel and clean up all resources */
destroy(): void;
/** Current slide index */
readonly currentIndex: number;
/** Whether the carousel is in fullscreen mode */
readonly isFullscreen: boolean;
/** Whether autoplay is paused */
readonly isAutoplayPaused: boolean;
}Usage example:
import { CloudImageCarousel } from '@cloudimage/carousel';
const carousel = new CloudImageCarousel('#my-carousel', {
images: [
{ src: 'photo1.jpg', alt: 'Living room' },
{ src: 'photo2.jpg', alt: 'Kitchen' },
{ src: 'photo3.jpg', alt: 'Bedroom' },
],
theme: 'light',
showBullets: true,
transitionEffect: 'slide',
onSlideChange(index) {
console.log('Slide changed to:', index);
},
});
carousel.init();All configuration is expressed via data-ci-carousel-* attributes on the container element. Images are passed as a JSON array in data-ci-carousel-images.
<div
data-ci-carousel-images='["photo1.jpg", "photo2.jpg", "photo3.jpg"]'
data-ci-carousel-theme="dark"
data-ci-carousel-show-bullets="true"
data-ci-carousel-transition="fade"
data-ci-carousel-autoplay="true"
data-ci-carousel-autoplay-interval="5000"
data-ci-carousel-show-thumbnails="true"
data-ci-carousel-controls-position="center"
data-ci-carousel-cycle="true"
data-ci-carousel-zoom-max="4"
data-ci-carousel-zoom-min="1"
data-ci-carousel-zoom-step="0.3"
></div>Auto-initialization (CDN usage):
<link rel="stylesheet" href="carousel.min.css" />
<script src="carousel.min.js"></script>
<script>CloudImageCarousel.autoInit();</script>CloudImageCarousel.autoInit() scans the DOM for all elements with data-ci-carousel-images and initializes each one. It returns an array of CloudImageCarousel instances.
CloudImageCarousel.autoInit(root?: HTMLElement | Document): CloudImageCarousel[];Attribute mapping:
| HTML Attribute | Config Property | Type |
|---|---|---|
data-ci-carousel-images |
images |
JSON string -> ImageSource[] |
data-ci-carousel-autoplay |
autoplay |
'true' | 'false' |
data-ci-carousel-autoplay-interval |
autoplayInterval |
string -> number |
data-ci-carousel-cycle |
cycle |
'true' | 'false' |
data-ci-carousel-show-filenames |
showFilenames |
'true' | 'false' |
data-ci-carousel-show-thumbnails |
showThumbnails |
'true' | 'false' |
data-ci-carousel-show-bullets |
showBullets |
'true' | 'false' |
data-ci-carousel-show-controls |
showControls |
'true' | 'false' |
data-ci-carousel-controls-position |
controlsPosition |
'center' | 'bottom' |
data-ci-carousel-theme |
theme |
'light' | 'dark' |
data-ci-carousel-transition |
transitionEffect |
'slide' | 'fade' | 'zoom' | 'flip' |
data-ci-carousel-zoom-min |
zoomMin |
string -> number |
data-ci-carousel-zoom-max |
zoomMax |
string -> number |
data-ci-carousel-zoom-step |
zoomStep |
string -> number |
data-ci-carousel-ci-token |
cloudimage.token |
string |
data-ci-carousel-ci-api-version |
cloudimage.apiVersion |
string |
data-ci-carousel-ci-domain |
cloudimage.domain |
string |
data-ci-carousel-ci-limit-factor |
cloudimage.limitFactor |
string -> number |
data-ci-carousel-ci-params |
cloudimage.params |
string |
Note: Callback options (
onSlideChange,onError) are only available via the JavaScript API, as functions cannot be expressed as HTML attributes. To attach callbacks to HTML-initialized instances, retrieve the instance fromautoInit()return value.
All visual customization is done via CSS custom properties. Consumers override colors and sizes by setting CSS variables on the .ci-carousel container or any ancestor element.
/* === Container === */
--ci-carousel-bg: #efefef;
--ci-carousel-border-radius: 16px;
--ci-carousel-shadow: 0 8px 43px rgba(0, 0, 0, 0.09);
/* === Controls === */
--ci-carousel-btn-size: 44px;
--ci-carousel-btn-bg: rgba(255, 255, 255, 0.9);
--ci-carousel-btn-color: #1a1a1a;
--ci-carousel-btn-hover-bg: rgba(255, 255, 255, 1);
--ci-carousel-btn-border: 1px solid rgba(0, 0, 0, 0.1);
--ci-carousel-btn-radius: 50%;
--ci-carousel-btn-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
--ci-carousel-controls-offset: 16px;
--ci-carousel-controls-transition: opacity 0.3s ease-in-out;
/* === Bullets === */
--ci-carousel-bullet-size: 8px;
--ci-carousel-bullet-bg: rgba(0, 0, 0, 0.4);
--ci-carousel-bullet-active-bg: #1a1a1a;
--ci-carousel-bullet-gap: 15px;
--ci-carousel-bullets-bg: #ffffff;
--ci-carousel-bullets-padding: 12px 16px;
/* === Thumbnails === */
--ci-carousel-thumbnail-width: 88px;
--ci-carousel-thumbnail-height: 66px;
--ci-carousel-thumbnail-radius: 12px;
--ci-carousel-thumbnail-active-border: 0 0 0 2px #000000;
--ci-carousel-thumbnails-bg: #ffffff;
--ci-carousel-thumbnails-gap: 16px;
--ci-carousel-thumbnails-padding: 24px;
--ci-carousel-thumbnails-border: none;
/* === Scroll Hint === */
--ci-carousel-hint-bg: rgba(0, 0, 0, 0.7);
--ci-carousel-hint-color: #ffffff;
/* === Filename Overlay === */
--ci-carousel-filename-color: #ffffff;
--ci-carousel-filename-bg: linear-gradient(to top, rgba(0, 0, 0, 0.75), transparent);
--ci-carousel-filename-size: 13px;
/* === Focus === */
--ci-carousel-focus-color: rgba(66, 153, 225, 0.6);
/* === Transitions === */
--ci-carousel-easing: cubic-bezier(0.16, 1, 0.3, 1);
--ci-carousel-transition-fast: 0.3s;
--ci-carousel-transition-normal: 0.5s;
--ci-carousel-transition-slow: 0.7s;
/* === Fullscreen === */
--ci-carousel-fullscreen-bg: rgba(0, 0, 0, 0.95);Custom theming example:
/* Brand-colored carousel */
.my-carousel .ci-carousel {
--ci-carousel-bg: #0f172a;
--ci-carousel-btn-bg: rgba(59, 130, 246, 0.85);
--ci-carousel-btn-color: #ffffff;
--ci-carousel-btn-hover-bg: rgba(59, 130, 246, 1);
--ci-carousel-bullet-active-bg: #3b82f6;
--ci-carousel-thumbnail-active-border: 0 0 0 2px #3b82f6;
--ci-carousel-thumbnails-bg: #0f172a;
}Themes are implemented as sets of CSS variable overrides. Setting theme: 'dark' (or data-ci-carousel-theme="dark") applies the ci-carousel-theme-dark class to the container, which activates dark variable values.
Dark theme overrides:
.ci-carousel-theme-dark {
--ci-carousel-bg: #1a1a2e;
--ci-carousel-btn-bg: rgba(255, 255, 255, 0.15);
--ci-carousel-btn-color: #f0f0f0;
--ci-carousel-btn-hover-bg: rgba(255, 255, 255, 0.25);
--ci-carousel-btn-border: 1px solid rgba(255, 255, 255, 0.1);
--ci-carousel-bullet-bg: rgba(255, 255, 255, 0.4);
--ci-carousel-bullet-active-bg: #ffffff;
--ci-carousel-thumbnails-bg: #1a1a2e;
--ci-carousel-thumbnail-active-border: 0 0 0 2px #ffffff;
}All animations and transitions respect the prefers-reduced-motion: reduce media query:
@media (prefers-reduced-motion: reduce) {
.ci-carousel,
.ci-carousel * {
animation: none !important;
transition-duration: 0.01ms !important;
}
}When reduced motion is preferred:
- Slide transitions are instant
- Zoom transitions are instant
- Control hover animations are disabled
- Autoplay still functions but transitions are instant
Zoom and pan use CSS transforms on the image wrapper element for GPU-accelerated rendering:
<div class="ci-carousel-main"> <- outer container (overflow: hidden)
<div class="ci-carousel-images-container">
<div class="ci-carousel-image-wrapper"> <- receives transform: scale() translate()
<img class="ci-carousel-image" />
</div>
</div>
</div>
The image wrapper receives:
transform: scale(var(--zoom)) translate(var(--pan-x), var(--pan-y));
transform-origin: 0 0;
will-change: transform;| Input | Behavior |
|---|---|
| Ctrl+scroll / Cmd+scroll | Zoom in/out centered on cursor position |
| Pinch gesture | Zoom in/out centered between two touch points |
| Double-click / Double-tap | Toggle between 1x and 2x zoom |
| Click-drag / Touch-drag | Pan when zoomed in (zoom level > 1) |
Keyboard + / = |
Zoom in |
Keyboard - |
Zoom out |
Keyboard 0 |
Reset zoom |
| Programmatic | carousel.zoomIn(), carousel.zoomOut(), carousel.resetZoom() |
- Min zoom: 1 (default) — configurable via
zoomMin - Max zoom: 4 (default) — configurable via
zoomMax - Zoom step: 0.3 (default) — configurable via
zoomStep - Pan boundaries: The image cannot be panned beyond its edges; the visible area always shows image content
- Smooth transitions: Zoom level changes animate with CSS transition
To prevent accidental zoom when users intend to scroll the page, the library gates wheel-based zoom behind a modifier key:
- Regular scroll (no modifier key): passes through to the page — the container does not intercept it
- Ctrl+scroll (or Cmd+scroll on Mac): triggers zoom in/out centered on cursor position
When a user scrolls without the modifier key over a zoom-enabled container, a scroll hint toast appears:
+-----------------------------------------+
| |
| [image] |
| |
| +---------------------------+ |
| | Ctrl + scroll or pinch | |
| | to zoom | |
| +---------------------------+ |
+-----------------------------------------+
- The hint auto-hides after a brief duration
aria-hidden="true"— decorative hint, not for screen readers
When navigation controls are shown (default), a fullscreen toggle button appears at the top-right corner of the container. It uses the browser's Fullscreen API with webkit prefixed fallbacks for Safari.
- Enter fullscreen: Container expands to fill the viewport with a dark background; image uses
object-fit: contain - Exit fullscreen: Returns to normal layout
- Icons: SVG maximize/minimize icons
- ARIA:
aria-label="Toggle fullscreen" - Keyboard:
Fkey toggles fullscreen - Focus trapping: Tab key cycles within the fullscreen container
- Graceful degradation: If the Fullscreen API is not supported by the browser, the button is not rendered
| Effect | Description |
|---|---|
slide |
Horizontal slide with previous image sliding out and next image sliding in |
fade |
Crossfade between images with opacity transition |
zoom |
Current image scales down while next image scales up into view |
flip |
3D card-flip rotation effect |
All transitions use the configurable easing and duration CSS variables:
--ci-carousel-easing: cubic-bezier(0.16, 1, 0.3, 1);
--ci-carousel-transition-normal: 0.5s;Each transition effect applies CSS classes to the active and incoming slides:
.ci-carousel-slide--active— currently visible slide.ci-carousel-slide--enter— incoming slide.ci-carousel-slide--exit— outgoing slide
The transition effect name is set as a data attribute on the images container, and CSS rules handle the animation per effect type.
import { CloudImageCarouselViewer, useCloudImageCarousel } from '@cloudimage/carousel/react';The React wrapper is a separate entry point to avoid bundling React for vanilla JS consumers. React is an optional peer dependency.
interface CloudImageCarouselViewerProps {
images?: ImageSource[];
autoplay?: boolean;
autoplayInterval?: number;
cycle?: boolean;
showFilenames?: boolean;
showThumbnails?: boolean;
showBullets?: boolean;
showControls?: boolean;
controlsPosition?: 'center' | 'bottom';
theme?: 'light' | 'dark';
transitionEffect?: 'slide' | 'fade' | 'zoom' | 'flip';
zoomMin?: number;
zoomMax?: number;
zoomStep?: number;
cloudimage?: CloudimageConfig;
onSlideChange?: (index: number) => void;
onError?: (src: string, index: number) => void;
className?: string;
style?: React.CSSProperties;
}Usage example:
import { CloudImageCarouselViewer } from '@cloudimage/carousel/react';
function Gallery() {
return (
<CloudImageCarouselViewer
images={['photo1.jpg', 'photo2.jpg', 'photo3.jpg']}
theme="dark"
showBullets
transitionEffect="slide"
onSlideChange={(index) => console.log('Slide:', index)}
/>
);
}Provides direct access to the vanilla CloudImageCarousel instance for imperative control:
import { useCloudImageCarousel } from '@cloudimage/carousel/react';
function Gallery() {
const { containerRef, instance } = useCloudImageCarousel({
images: ['photo1.jpg', 'photo2.jpg'],
controlsPosition: 'bottom',
});
return (
<>
<div ref={containerRef} />
<button onClick={() => instance?.next()}>Next</button>
<button onClick={() => instance?.zoomIn()}>Zoom In</button>
</>
);
}The <CloudImageCarouselViewer> component forwards a ref exposing instance methods:
import { useRef } from 'react';
import { CloudImageCarouselViewer, CloudImageCarouselViewerRef } from '@cloudimage/carousel/react';
function Gallery() {
const ref = useRef<CloudImageCarouselViewerRef>(null);
return (
<>
<CloudImageCarouselViewer ref={ref} images={['photo1.jpg', 'photo2.jpg']} />
<button onClick={() => ref.current?.next()}>Next</button>
<button onClick={() => ref.current?.zoomIn()}>Zoom In</button>
<button onClick={() => ref.current?.setTheme('dark')}>Dark</button>
</>
);
}The React wrapper is SSR-safe:
- The vanilla core is instantiated inside
useEffect(client-only) - No
window,document, ornavigatoraccess during server rendering - The component renders an empty container
<div>on the server; hydration attaches the carousel
The library targets WCAG 2.1 Level AA conformance across all interactive elements.
| Key | Action |
|---|---|
ArrowLeft |
Navigate to previous slide |
ArrowRight |
Navigate to next slide |
+ / = |
Zoom in |
- |
Zoom out |
0 |
Reset zoom |
F |
Toggle fullscreen |
Escape |
Reset zoom; exit fullscreen |
Tab |
Navigate between interactive elements |
Shift + Tab |
Navigate backwards between interactive elements |
Enter / Space |
Activate focused button (thumbnail, bullet, control) |
Carousel container:
<div class="ci-carousel" role="region" aria-roledescription="carousel" aria-label="Image carousel">Slide:
<div class="ci-carousel-image-wrapper" role="group" aria-roledescription="slide" aria-label="Slide 1 of 5">
<img class="ci-carousel-image" alt="Living room" />
</div>Navigation buttons:
<button class="ci-carousel-prev" aria-label="Previous slide">
<button class="ci-carousel-next" aria-label="Next slide">
<button class="ci-carousel-fullscreen" aria-label="Toggle fullscreen">Bullet indicators:
<button class="ci-carousel-bullet" aria-label="Go to slide 1" aria-pressed="true">Thumbnails:
<button class="ci-carousel-thumbnail" aria-label="Go to slide 1" aria-pressed="true">
<img alt="Living room thumbnail" />
</button>A visually hidden live region announces slide changes to screen readers:
<div class="ci-carousel-sr-only" aria-live="polite" aria-atomic="true">
Slide 2 of 5: Kitchen
</div>- Focus ring: Interactive elements display a visible focus ring when focused via keyboard (
:focus-visible), styled withoutlineusing--ci-carousel-focus-color - Focus trapping in fullscreen: When fullscreen is active,
Tabcycles within the carousel container to prevent focus from escaping behind the fullscreen overlay - Autoplay pause on focus: When any carousel element receives keyboard focus, autoplay pauses automatically (WCAG 2.2.2)
All animations and transitions respect the prefers-reduced-motion: reduce media query (see Section 4.3).
Vite is used as the build tool in library mode, following the pattern established by Scaleflex's cloudimage-hotspot project.
| Format | File | Use Case |
|---|---|---|
| ESM | dist/carousel.esm.js |
Modern bundlers (Webpack, Vite, Rollup) |
| CJS | dist/carousel.cjs.js |
Node.js, legacy bundlers |
| UMD | dist/carousel.min.js |
CDN <script> tag, exposes window.CloudImageCarousel |
| CSS | dist/carousel.min.css |
Stylesheet (also inlined in UMD) |
| TypeScript | dist/index.d.ts |
Type definitions |
| React ESM | dist/react/index.js |
React wrapper (ESM) |
| React CJS | dist/react/index.cjs |
React wrapper (CJS) |
| React Types | dist/react/index.d.ts |
React wrapper type definitions |
{
"name": "@cloudimage/carousel",
"version": "1.0.1",
"description": "A JavaScript carousel/image viewer plugin by CloudImage (Scaleflex)",
"license": "MIT",
"author": "CloudImage by Scaleflex",
"main": "dist/carousel.cjs.js",
"module": "dist/carousel.esm.js",
"unpkg": "dist/carousel.min.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/carousel.esm.js",
"require": "./dist/carousel.cjs.js"
},
"./react": {
"types": "./dist/react/index.d.ts",
"import": "./dist/react/index.js",
"require": "./dist/react/index.cjs"
}
},
"files": ["dist"],
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
},
"peerDependenciesMeta": {
"react": { "optional": true },
"react-dom": { "optional": true }
}
}| Script | Description |
|---|---|
dev |
Start Vite dev server with demo page |
dev:react |
Start Vite dev server with React demo |
build |
Build all formats (main bundle + React wrapper + type declarations) |
build:bundle |
Build main bundle only (ESM + CJS + UMD) |
build:react |
Build React wrapper only |
build:demo |
Build GitHub Pages demo site |
typecheck |
Run TypeScript type checking |
typecheck:emit |
Emit type declarations to dist/ |
test |
Run tests with Vitest |
test:watch |
Run tests in watch mode |
test:coverage |
Run tests with coverage report |
lint |
Run ESLint |
format |
Format all files with Prettier |
format:check |
Check formatting |
| Bundle | Target |
|---|---|
| Core (UMD, minified + gzipped) | < 15 KB |
| Core (ESM, minified + gzipped) | < 12 KB |
| React wrapper (ESM, minified + gzipped) | < 3 KB |
| CSS (minified + gzipped) | < 5 KB |
The library has zero runtime dependencies. All functionality — zoom/pan, transitions, swipe gestures, fullscreen, lazy loading — is implemented within the library itself.
@cloudimage/carousel/
├── src/
│ ├── index.ts # Main entry — exports CloudImageCarousel
│ ├── core/
│ │ ├── carousel.ts # Core class: lifecycle, navigation, public API
│ │ ├── config.ts # Config merging/validation
│ │ └── types.ts # TypeScript interfaces and types
│ ├── controls/
│ │ ├── controls.ts # Nav buttons (prev/next), fullscreen, keyboard
│ │ ├── swipe.controls.ts # Touch/swipe gestures
│ │ └── zoom-pan.controls.ts # Custom zoom/pan implementation
│ ├── constants/
│ │ ├── classes.constants.ts # CSS class name strings
│ │ ├── controls.constants.ts # Control-related constants + keyboard keys
│ │ ├── events.constants.ts # DOM event name strings
│ │ ├── icons.constants.ts # SVG icon markup
│ │ ├── transition.constants.ts # Transition effect names
│ │ └── index.ts # Barrel export
│ ├── fullscreen/
│ │ └── fullscreen.ts # Fullscreen control (uses Fullscreen API)
│ ├── utils/
│ │ ├── cloudimage.ts # Cloudimage CDN URL transforms
│ │ ├── dom.utils.ts # DOM helper functions
│ │ ├── image.utils.ts # Image URL utilities
│ │ ├── throttling.utils.ts # Throttle/debounce
│ │ └── index.ts # Barrel export
│ ├── styles/
│ │ └── index.css # Complete consolidated stylesheet
│ └── react/
│ ├── cloud-image-carousel-viewer.tsx # React component wrapper
│ ├── use-cloud-image-carousel.ts # React hook
│ ├── types.ts # React-specific types
│ └── index.ts # Barrel export
├── demo/
│ ├── index.html # Interactive demo page (GitHub Pages)
│ ├── demo.css # Demo-specific layout styles
│ ├── demo.ts # Demo initialization
│ ├── configurator.ts # Interactive playground with code generation
│ └── react-demo/
│ ├── index.html # React demo entry
│ ├── App.tsx # React demo application
│ └── main.tsx # React demo mount
├── examples/
│ ├── vanilla/
│ │ ├── index.html # Vanilla JS CodeSandbox example
│ │ ├── index.js # Vanilla JS example code
│ │ ├── package.json # Example dependencies
│ │ └── sandbox.config.json # CodeSandbox config
│ └── react/
│ ├── index.html # React CodeSandbox example
│ ├── package.json # Example dependencies
│ └── src/
│ ├── App.tsx # React example app
│ └── main.tsx # React example mount
├── config/
│ ├── vite.config.ts # Main bundle build config
│ ├── vite.react.config.ts # React wrapper build config
│ ├── vite.dev.config.ts # Dev server config
│ ├── vite.react-demo.config.ts # React demo dev server config
│ └── vite.demo.config.ts # Demo build config (GitHub Pages)
├── dist/ # Built output (CDN bundles committed)
│ ├── carousel.min.js
│ └── carousel.min.js.map
├── .github/
│ └── workflows/
│ └── deploy-pages.yml # GitHub Pages deployment workflow
├── package.json
├── tsconfig.json
├── tsconfig.build.json
├── .eslintrc.cjs
├── .prettierrc.json
├── .gitignore
├── LICENSE
├── README.md
├── CHANGELOG.md
└── SPECS.md # This file
The demo site is hosted at https://scaleflex.github.io/cloudimage-carousel/ and deployed via GitHub Actions on push to master.
| Section | Description |
|---|---|
| Hero | Gradient background with animated heading, feature pills (Zoom & Pan, 4 Transitions, WCAG 2.1 AA, Dark Mode, React Ready), dual CTA buttons (Get Started / GitHub), and a live carousel viewer |
| Getting Started | Side-by-side npm and CDN installation cards with dark-themed code blocks and copy-to-clipboard |
| Transition Effects | Interactive comparison of all four transition effects (slide, fade, zoom, flip) with live examples |
| Themes | Light and dark theme demonstrations |
| Interactive Configurator | Two-panel layout (controls + preview) with toggles and selects for all config options, real-time generated code with copy button |
| Footer | Scaleflex logo, links to documentation, GitHub, npm |
A panel that lets visitors:
- Toggle configuration options: autoplay, cycle, show thumbnails, show bullets, show filenames, show controls
- Select values: theme, transition effect, controls position
- Configure zoom: min, max, step
- See the generated JavaScript code update in real-time
- Copy the generated code to clipboard
Demo images are high-quality, royalty-free photographs served via Scaleflex CDN.
Images use IntersectionObserver to defer loading until the carousel container enters the viewport. A data-src attribute holds the real image URL, swapped to src when the container becomes visible.
- Enabled by default
- Falls back to eager loading if
IntersectionObserveris not available - The container maintains its aspect ratio to prevent layout shift
When a cloudimage configuration is provided with a valid token, the library automatically requests optimally-sized images from the Scaleflex Cloudimage CDN.
- Detect container width: Read
container.offsetWidthto determine the displayed image width - Multiply by device pixel ratio:
requestedWidth = containerWidth * window.devicePixelRatio - Round to
limitFactor: RoundrequestedWidthup to the nearestlimitFactor(default: 100px) for better CDN cache hit rates - Build Cloudimage URL: Construct the optimized image URL
- Set as
<img>src: The built URL replaces the rawsrcon the image element
https://{token}.{domain}/{apiVersion}/{src}?width={requestedWidth}&{params}
With defaults:
https://demo.cloudimg.io/v7/https://example.com/photo.jpg?width=800
If cloudimage is not provided in the config, or cloudimage.token is falsy, the library uses the raw src URL unchanged.
A ResizeObserver monitors the container element. When the container width changes such that the rounded requested width crosses a limitFactor boundary, new Cloudimage URLs are built and image src attributes are updated.
When zoom level > 1, the requested image width accounts for the zoom level to maintain sharpness:
requestedWidth = containerWidth * zoomLevel * devicePixelRatio
When an image fails to load, the carousel:
- Fires the
onError(src, index)callback if provided - Displays a graceful fallback state for the broken image
- Does not break navigation — other slides remain fully functional
Autoplay advances slides automatically at a configurable interval (default: 3000ms, minimum: 100ms).
- Pause on hover: Autoplay pauses when the cursor enters the carousel
- Pause on focus: Autoplay pauses when any carousel element receives keyboard focus
- Resume on leave: Autoplay resumes when cursor/focus leaves the carousel
- Programmatic control:
startAutoplay(),stopAutoplay(),pauseAutoplay(),resumeAutoplay() - WCAG 2.2.2: Users can pause, stop, or hide auto-updating content
Event callbacks enable integration with analytics platforms:
const carousel = new CloudImageCarousel('#el', {
images: ['photo1.jpg', 'photo2.jpg'],
onSlideChange(index) {
analytics.track('carousel_slide', { index });
},
onError(src, index) {
analytics.track('image_error', { src, index });
},
});| Feature | @cloudimage/carousel | Swiper | Slick | Flickity | Splide |
|---|---|---|---|---|---|
| Zero dependencies | Yes | Yes | jQuery | jQuery | Yes |
| TypeScript | Full | Full | No | No | Full |
| Zoom & pan built-in | Yes | Plugin | No | No | No |
| WCAG 2.1 AA | Yes | Partial | No | Partial | Yes |
| CSS variable theming | Yes | Yes | No | No | Partial |
| React wrapper | Yes | Yes | Community | Community | Yes |
| Data-attr init | Yes | No | No | No | Yes |
| Transitions | 4 types | Custom | 1 | 1 | 2 |
| Cloudimage CDN | Native | No | No | No | No |
| Bundle size (gzip) | < 15 KB | ~40 KB | ~25 KB | ~10 KB | ~15 KB |
- Video slides — support for video sources alongside images
- Lightbox mode — overlay gallery with backdrop
- Lazy thumbnail loading — defer thumbnail loading for large galleries
- RTL support — right-to-left layout and navigation
- Virtual slides — only render visible slides for large galleries (100+ images)
- Vue wrapper —
<CICarousel>component for Vue 3 - Svelte wrapper —
<CICarousel>component for Svelte - Adaptive image loading — connection-aware quality selection