Programmatic Control
All zoom modes provide methods to control zoom behavior programmatically.Controlling Wheel Zoom State
Wheel zoom mode offers the most programmatic control through itssetState and getState methods.
import { createZoomImageWheel } from "@zoom-image/core";
const container = document.getElementById("container");
const result = createZoomImageWheel(container, {
maxZoom: 8,
});
// Get current state
const currentState = result.getState();
console.log('Current zoom:', currentState.currentZoom);
console.log('Position:', currentState.currentPositionX, currentState.currentPositionY);
console.log('Rotation:', currentState.currentRotation);
// Programmatically zoom in
function zoomIn() {
result.setState({
currentZoom: result.getState().currentZoom + 0.5,
});
}
// Programmatically zoom out
function zoomOut() {
result.setState({
currentZoom: result.getState().currentZoom - 0.5,
});
}
// Reset zoom
function resetZoom() {
result.setState({
currentZoom: 1,
});
}
// Rotate image
function rotate(degrees) {
result.setState({
currentRotation: result.getState().currentRotation + degrees,
});
}
// Enable/disable zoom
function toggleZoom() {
result.setState({
enable: !result.getState().enable,
});
}
Animated Zoom Transitions
Create smooth animated zoom effects:function animateZoomTo(targetZoom, duration = 300) {
const startZoom = result.getState().currentZoom;
const startTime = performance.now();
function easeInOutQuad(t) {
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
function animate(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easedProgress = easeInOutQuad(progress);
const currentZoom = startZoom + (targetZoom - startZoom) * easedProgress;
result.setState({ currentZoom });
if (progress < 1) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
// Usage
animateZoomTo(3, 500); // Zoom to 300% over 500ms
Preset Zoom Levels
Implement preset zoom buttons:const presets = [1, 2, 4, 6];
let currentPresetIndex = 0;
function nextZoomPreset() {
currentPresetIndex = (currentPresetIndex + 1) % presets.length;
result.setState({
currentZoom: presets[currentPresetIndex],
});
}
function previousZoomPreset() {
currentPresetIndex = (currentPresetIndex - 1 + presets.length) % presets.length;
result.setState({
currentZoom: presets[currentPresetIndex],
});
}
Controlling Hover Zoom
Hover zoom has limited programmatic control but you can enable/disable it:import { createZoomImageHover } from "@zoom-image/core";
const result = createZoomImageHover(container, {
zoomTarget: document.getElementById('zoom-target'),
customZoom: { width: 400, height: 600 },
scale: 2,
});
// Toggle zoom on/off
function toggleHoverZoom() {
const currentState = result.getState();
result.setState({
enabled: !currentState.enabled,
});
}
Lifecycle Management
Proper lifecycle management prevents memory leaks and ensures smooth performance.Basic Cleanup
const result = createZoomImageWheel(container);
// Subscribe to state
const unsubscribe = result.subscribe(({ state }) => {
console.log('State changed:', state);
});
// When done (e.g., component unmounting, page navigation)
function cleanup() {
unsubscribe(); // Stop listening to state changes
result.cleanup(); // Remove event listeners and clean up DOM
}
Cleanup with Event Listeners
UseAbortController for managing additional event listeners:
const container = document.getElementById("container");
const result = createZoomImageWheel(container);
const controller = new AbortController();
const { signal } = controller;
// Add custom event listeners
const zoomInBtn = document.getElementById("zoom-in");
zoomInBtn.addEventListener('click', () => {
result.setState({ currentZoom: result.getState().currentZoom + 0.5 });
}, { signal });
const zoomOutBtn = document.getElementById("zoom-out");
zoomOutBtn.addEventListener('click', () => {
result.setState({ currentZoom: result.getState().currentZoom - 0.5 });
}, { signal });
// Subscribe to state
const unsubscribe = result.subscribe(({ state }) => {
document.getElementById('zoom-level').textContent =
`${Math.round(state.currentZoom * 100)}%`;
});
// Cleanup everything
function cleanup() {
controller.abort(); // Remove all custom event listeners
unsubscribe(); // Unsubscribe from state
result.cleanup(); // Clean up zoom instance
}
Single Page Application Pattern
For SPA frameworks (without using framework adapters):class ZoomImageComponent {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.zoomInstance = null;
this.unsubscribe = null;
this.controller = new AbortController();
}
mount() {
// Create zoom instance
this.zoomInstance = createZoomImageWheel(this.container, {
maxZoom: 6,
});
// Subscribe to state
this.unsubscribe = this.zoomInstance.subscribe(({ state }) => {
this.onStateChange(state);
});
// Add event listeners
this.setupControls();
}
setupControls() {
const { signal } = this.controller;
document.getElementById('zoom-in').addEventListener('click', () => {
this.zoomInstance.setState({
currentZoom: this.zoomInstance.getState().currentZoom + 0.5,
});
}, { signal });
document.getElementById('zoom-out').addEventListener('click', () => {
this.zoomInstance.setState({
currentZoom: this.zoomInstance.getState().currentZoom - 0.5,
});
}, { signal });
}
onStateChange(state) {
console.log('Zoom level:', state.currentZoom);
}
unmount() {
// Clean up in reverse order
this.controller.abort();
this.unsubscribe?.();
this.zoomInstance?.cleanup();
this.zoomInstance = null;
this.unsubscribe = null;
this.controller = new AbortController();
}
}
// Usage
const component = new ZoomImageComponent('container');
component.mount();
// Later, when navigating away
component.unmount();
Multiple Zoom Instances
Manage multiple independent zoom instances on the same page.Multiple Instances of Same Type
import { createZoomImageHover } from "@zoom-image/core";
const products = [
{ id: 'product-1', container: 'container-1', target: 'target-1' },
{ id: 'product-2', container: 'container-2', target: 'target-2' },
{ id: 'product-3', container: 'container-3', target: 'target-3' },
];
const zoomInstances = new Map();
products.forEach(product => {
const container = document.getElementById(product.container);
const zoomTarget = document.getElementById(product.target);
const instance = createZoomImageHover(container, {
zoomTarget: zoomTarget,
customZoom: { width: 300, height: 400 },
scale: 2,
});
zoomInstances.set(product.id, instance);
});
// Cleanup all instances
function cleanupAll() {
zoomInstances.forEach(instance => instance.cleanup());
zoomInstances.clear();
}
Gallery with Different Zoom Modes
import {
createZoomImageWheel,
createZoomImageHover,
createZoomImageMove,
} from "@zoom-image/core";
class ImageGallery {
constructor() {
this.instances = [];
}
addWheelZoom(containerId) {
const container = document.getElementById(containerId);
const instance = createZoomImageWheel(container, {
maxZoom: 4,
});
this.instances.push(instance);
return instance;
}
addHoverZoom(containerId, targetId) {
const container = document.getElementById(containerId);
const target = document.getElementById(targetId);
const instance = createZoomImageHover(container, {
zoomTarget: target,
customZoom: { width: 400, height: 600 },
scale: 2,
});
this.instances.push(instance);
return instance;
}
addMoveZoom(containerId) {
const container = document.getElementById(containerId);
const instance = createZoomImageMove(container, {
zoomFactor: 4,
});
this.instances.push(instance);
return instance;
}
cleanupAll() {
this.instances.forEach(instance => instance.cleanup());
this.instances = [];
}
}
// Usage
const gallery = new ImageGallery();
gallery.addWheelZoom('main-product');
gallery.addHoverZoom('thumbnail-1', 'zoom-target-1');
gallery.addHoverZoom('thumbnail-2', 'zoom-target-2');
gallery.addMoveZoom('detail-view');
// Later
gallery.cleanupAll();
Synchronized Zoom Instances
Synchronize zoom state across multiple instances:const container1 = document.getElementById('container-1');
const container2 = document.getElementById('container-2');
const instance1 = createZoomImageWheel(container1);
const instance2 = createZoomImageWheel(container2);
// Synchronize zoom level
instance1.subscribe(({ state }) => {
instance2.setState({
currentZoom: state.currentZoom,
currentRotation: state.currentRotation,
});
});
instance2.subscribe(({ state }) => {
instance1.setState({
currentZoom: state.currentZoom,
currentRotation: state.currentRotation,
});
});
Be careful with bidirectional synchronization to avoid infinite loops. Consider using a flag or checking if the state actually changed before synchronizing.
Image Cropping
Capture the current zoomed view as a cropped image.Basic Cropping
import { createZoomImageWheel, cropImage } from "@zoom-image/core";
const container = document.getElementById("container");
const result = createZoomImageWheel(container);
async function handleCrop() {
const state = result.getState();
const croppedImageUrl = await cropImage({
image: container.querySelector('img'),
currentZoom: state.currentZoom,
positionX: state.currentPositionX,
positionY: state.currentPositionY,
rotation: state.currentRotation,
});
// Use the cropped image
const img = document.getElementById('cropped-result');
img.src = croppedImageUrl;
img.hidden = false;
}
document.getElementById('crop-btn').addEventListener('click', handleCrop);
Download Cropped Image
async function downloadCroppedImage() {
const state = result.getState();
const croppedImageUrl = await cropImage({
image: container.querySelector('img'),
currentZoom: state.currentZoom,
positionX: state.currentPositionX,
positionY: state.currentPositionY,
rotation: state.currentRotation,
});
// Create download link
const link = document.createElement('a');
link.href = croppedImageUrl;
link.download = `cropped-image-${Date.now()}.png`;
link.click();
}
Upload Cropped Image
async function uploadCroppedImage() {
const state = result.getState();
const croppedImageUrl = await cropImage({
image: container.querySelector('img'),
currentZoom: state.currentZoom,
positionX: state.currentPositionX,
positionY: state.currentPositionY,
rotation: state.currentRotation,
});
// Convert data URL to blob
const response = await fetch(croppedImageUrl);
const blob = await response.blob();
// Upload
const formData = new FormData();
formData.append('image', blob, 'cropped.png');
await fetch('/api/upload', {
method: 'POST',
body: formData,
});
}
Crop with Custom Dimensions
ThecropImage function respects the rotation and current viewport:
async function cropWithRotation() {
const state = result.getState();
const img = container.querySelector('img');
const croppedUrl = await cropImage({
image: img,
currentZoom: state.currentZoom,
positionX: state.currentPositionX,
positionY: state.currentPositionY,
rotation: state.currentRotation, // 0, 90, 180, or 270
});
// The cropped image will be rotated accordingly
return croppedUrl;
}
Integration Patterns
Modal/Lightbox Integration
class ZoomLightbox {
constructor() {
this.zoomInstance = null;
this.controller = new AbortController();
}
open(imageSrc) {
// Create modal
const modal = document.createElement('div');
modal.id = 'zoom-modal';
modal.className = 'fixed inset-0 bg-black/90 z-50 flex items-center justify-center';
modal.innerHTML = `
<button id="close-modal" class="absolute top-4 right-4 text-white">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div id="zoom-container" class="w-[80vw] h-[80vh]">
<img src="${imageSrc}" alt="Zoomed image" class="w-full h-full object-contain" />
</div>
`;
document.body.appendChild(modal);
// Initialize zoom
const container = document.getElementById('zoom-container');
this.zoomInstance = createZoomImageWheel(container, {
maxZoom: 8,
});
// Close on button click
const { signal } = this.controller;
document.getElementById('close-modal').addEventListener('click', () => {
this.close();
}, { signal });
// Close on escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.close();
}, { signal });
}
close() {
this.controller.abort();
this.zoomInstance?.cleanup();
document.getElementById('zoom-modal')?.remove();
this.zoomInstance = null;
this.controller = new AbortController();
}
}
// Usage
const lightbox = new ZoomLightbox();
document.querySelectorAll('.gallery-image').forEach(img => {
img.addEventListener('click', () => {
lightbox.open(img.src);
});
});
E-commerce Product Viewer
class ProductViewer {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.mainZoom = null;
this.thumbnailZooms = [];
this.currentImageIndex = 0;
this.images = [];
}
init(images) {
this.images = images;
this.renderMainImage();
this.renderThumbnails();
}
renderMainImage() {
const mainContainer = this.container.querySelector('.main-image');
mainContainer.innerHTML = `<img src="${this.images[this.currentImageIndex]}" alt="Product" />`;
this.mainZoom?.cleanup();
this.mainZoom = createZoomImageWheel(mainContainer, {
maxZoom: 6,
wheelZoomRatio: 0.1,
});
}
renderThumbnails() {
const thumbnailContainer = this.container.querySelector('.thumbnails');
thumbnailContainer.innerHTML = '';
this.thumbnailZooms.forEach(zoom => zoom.cleanup());
this.thumbnailZooms = [];
this.images.forEach((src, index) => {
const wrapper = document.createElement('div');
wrapper.className = 'thumbnail';
wrapper.innerHTML = `<img src="${src}" alt="Thumbnail ${index + 1}" />`;
wrapper.addEventListener('click', () => {
this.currentImageIndex = index;
this.renderMainImage();
});
const zoomTarget = document.createElement('div');
zoomTarget.className = 'thumbnail-zoom-target';
wrapper.appendChild(zoomTarget);
const thumbnailZoom = createZoomImageHover(wrapper, {
zoomTarget: zoomTarget,
customZoom: { width: 200, height: 200 },
scale: 1.5,
});
this.thumbnailZooms.push(thumbnailZoom);
thumbnailContainer.appendChild(wrapper);
});
}
cleanup() {
this.mainZoom?.cleanup();
this.thumbnailZooms.forEach(zoom => zoom.cleanup());
}
}
// Usage
const viewer = new ProductViewer('product-viewer');
viewer.init([
'/images/product-1.jpg',
'/images/product-2.jpg',
'/images/product-3.jpg',
]);
Responsive Zoom Mode Switching
Switch between zoom modes based on viewport size:class ResponsiveZoom {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.currentInstance = null;
this.currentMode = null;
}
init() {
this.updateZoomMode();
window.addEventListener('resize', () => this.updateZoomMode());
}
updateZoomMode() {
const width = window.innerWidth;
let newMode;
if (width < 768) {
newMode = 'click'; // Mobile: click to zoom
} else if (width < 1024) {
newMode = 'move'; // Tablet: move to zoom
} else {
newMode = 'hover'; // Desktop: hover to zoom
}
if (newMode !== this.currentMode) {
this.currentInstance?.cleanup();
this.currentMode = newMode;
this.initZoomMode(newMode);
}
}
initZoomMode(mode) {
switch (mode) {
case 'hover':
const target = document.getElementById('zoom-target');
this.currentInstance = createZoomImageHover(this.container, {
zoomTarget: target,
customZoom: { width: 400, height: 600 },
scale: 2,
});
break;
case 'move':
this.currentInstance = createZoomImageMove(this.container, {
zoomFactor: 4,
});
break;
case 'click':
this.currentInstance = createZoomImageClick(this.container, {
zoomFactor: 4,
});
break;
}
}
cleanup() {
this.currentInstance?.cleanup();
}
}
// Usage
const responsiveZoom = new ResponsiveZoom('container');
responsiveZoom.init();
Performance Optimization
Lazy load zoom instances
Lazy load zoom instances
Only initialize zoom when the image becomes visible:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const container = entry.target;
const instance = createZoomImageWheel(container);
observer.unobserve(container);
}
});
});
document.querySelectorAll('.zoom-container').forEach(container => {
observer.observe(container);
});
Debounce state updates
Debounce state updates
When displaying zoom information, debounce updates to reduce re-renders:
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
const updateZoomInfo = debounce((state) => {
document.getElementById('zoom-level').textContent =
`${Math.round(state.currentZoom * 100)}%`;
}, 16); // ~60fps
result.subscribe(({ state }) => {
updateZoomInfo(state);
});
Preload high-resolution images
Preload high-resolution images
Preload zoom images before they’re needed:
function preloadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
// Preload on hover
container.addEventListener('mouseenter', async () => {
await preloadImage('/images/product-high-res.jpg');
// Image is now cached
}, { once: true });
Next Steps
Vanilla JS Guide
Complete vanilla JavaScript examples
Configuration
All configuration options explained
React Integration
Use Zoom Image with React
API Reference
Detailed API documentation