Skip to main content
This guide covers advanced usage patterns for building sophisticated zoom image experiences.

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 its setState 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

Use AbortController 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();
}
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

The cropImage 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

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);
});
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 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

Build docs developers (and LLMs) love