E-commerce & Retail

Free Product Image Zoom Widget

Hover-to-zoom product image viewer with a magnifier lens or inner pan.

Product Image Zoom adds a hover-to-zoom viewer to any product photo, with a choice of a magnifier lens or an inner-zoom pan effect. It is built for e-commerce stores that want shoppers to inspect product detail without opening a new page. Paste one script tag with your image URL and the widget renders right where you place it.

Live demo

Embed this widget

Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.

Hosted (recommended)html
<script src="https://hayneslab.dev/widgets/product-image-zoom.js" data-hl-src="https://placehold.co/800x600" data-hl-alt="Demo product photo" async></script>
<!-- Free Product Image Zoom widget by haynes lab — https://hayneslab.dev/tools/widgets/product-image-zoom -->
Inline (self-contained)html
<!-- Free Product Image Zoom widget by haynes lab — https://hayneslab.dev/tools/widgets/product-image-zoom -->
<script data-hl-src="https://placehold.co/800x600" data-hl-alt="Demo product photo">
/*!
 * haynes lab — Product Image Zoom widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/product-image-zoom
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-product-image-zoom';
  var STYLE_ID = 'hlw-product-image-zoom-styles';
  var LENS_SIZE = 160;

  function findScript() {
    return document.currentScript ||
      document.querySelector('script[src*="widgets/product-image-zoom.js"]');
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; display: inline-block; max-width: 100%; margin: 0;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' border: 1px solid #e5e7eb; border-radius: 12px; overflow: hidden; background: #f9fafb;' +
      ' cursor: crosshair; line-height: 0; }' +
      '.' + PREFIX + '__img { display: block; width: 100%; height: auto; }' +
      '.' + PREFIX + '--inner .' + PREFIX + '__img { transition: transform 0.25s ease; will-change: transform; }' +
      '.' + PREFIX + '__lens { position: absolute; width: ' + LENS_SIZE + 'px; height: ' + LENS_SIZE + 'px;' +
      ' border: 2px solid #ffffff; border-radius: 50%; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);' +
      ' background-repeat: no-repeat; pointer-events: none; opacity: 0; transition: opacity 0.15s ease; }' +
      '.' + PREFIX + '__lens--on { opacity: 1; }' +
      '.' + PREFIX + '__credit { position: absolute; right: 10px; bottom: 8px; font-size: 11px; line-height: 1.4;' +
      ' display: inline-flex; align-items: center; gap: 4px; color: #9ca3af; text-decoration: none;' +
      ' background: rgba(255, 255, 255, 0.85); padding: 2px 6px; border-radius: 6px; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function el(tag, className, text) {
    var node = document.createElement(tag);
    if (className) node.className = className;
    if (text != null) node.textContent = text;
    return node;
  }

  function logoSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 32 32');
    svg.setAttribute('width', '13');
    svg.setAttribute('height', '13');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    function circle(cx, cy, r, attrs) {
      var c = document.createElementNS(svgNS, 'circle');
      c.setAttribute('cx', cx);
      c.setAttribute('cy', cy);
      c.setAttribute('r', r);
      for (var key in attrs) c.setAttribute(key, attrs[key]);
      svg.appendChild(c);
    }
    circle(16, 16, 16, { fill: '#0f0a0a' });
    circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
    circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
    circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
    circle(16, 16, 3, { fill: '#06b6d4' });
    circle(24.5, 7.5, 2, { fill: '#f97316' });
    circle(7, 16, 1.8, { fill: '#f97316' });
    circle(20, 20, 1.5, { fill: '#f97316' });
    return svg;
  }

  function clamp(v, min, max) {
    return v < min ? min : (v > max ? max : v);
  }

  function build(script) {
    var src = script.getAttribute('data-hl-src');
    if (!src) {
      console.warn('[haynes lab product-image-zoom] Missing required data-hl-src attribute (image URL). Nothing rendered.');
      return;
    }
    var alt = script.getAttribute('data-hl-alt') || '';
    var zoom = parseFloat(script.getAttribute('data-hl-zoom'));
    if (isNaN(zoom) || zoom <= 1) zoom = 2.5;
    var mode = script.getAttribute('data-hl-mode') === 'inner' ? 'inner' : 'lens';

    injectStyles();

    var root = el('figure', PREFIX + (mode === 'inner' ? ' ' + PREFIX + '--inner' : ''));
    var img = el('img', PREFIX + '__img');
    img.src = src;
    img.alt = alt;
    img.setAttribute('draggable', 'false');
    root.appendChild(img);

    var lens = null;
    if (mode === 'lens') {
      lens = el('div', PREFIX + '__lens');
      lens.style.backgroundImage = 'url("' + src.replace(/"/g, '%22') + '")';
      root.appendChild(lens);
    }

    function pointFromEvent(e) {
      var rect = root.getBoundingClientRect();
      return {
        x: clamp(e.clientX - rect.left, 0, rect.width),
        y: clamp(e.clientY - rect.top, 0, rect.height),
        w: rect.width,
        h: rect.height
      };
    }

    function lensMove(e) {
      var p = pointFromEvent(e);
      var r = LENS_SIZE / 2;
      lens.style.left = (p.x - r) + 'px';
      lens.style.top = (p.y - r) + 'px';
      lens.style.backgroundSize = (p.w * zoom) + 'px ' + (p.h * zoom) + 'px';
      lens.style.backgroundPosition = (-(p.x * zoom - r)) + 'px ' + (-(p.y * zoom - r)) + 'px';
    }

    function innerMove(e) {
      var p = pointFromEvent(e);
      img.style.transformOrigin = (p.x / p.w * 100) + '% ' + (p.y / p.h * 100) + '%';
      img.style.transform = 'scale(' + zoom + ')';
    }

    function innerReset() {
      img.style.transform = '';
    }

    root.addEventListener('pointerenter', function (e) {
      if (e.pointerType === 'touch') return;
      if (lens) {
        lens.classList.add(PREFIX + '__lens--on');
        lensMove(e);
      } else {
        innerMove(e);
      }
    });
    root.addEventListener('pointermove', function (e) {
      if (e.pointerType === 'touch') return;
      if (lens) {
        lens.classList.add(PREFIX + '__lens--on');
        lensMove(e);
      } else {
        innerMove(e);
      }
    });
    root.addEventListener('pointerleave', function (e) {
      if (e.pointerType === 'touch') return;
      if (lens) {
        lens.classList.remove(PREFIX + '__lens--on');
      } else {
        innerReset();
      }
    });

    // Touch devices: tap-and-drag pans an inner-style zoom regardless of mode.
    root.addEventListener('touchstart', function (e) {
      if (e.touches.length !== 1) return;
      innerMove(e.touches[0]);
    }, { passive: true });
    root.addEventListener('touchmove', function (e) {
      if (e.touches.length !== 1) return;
      innerMove(e.touches[0]);
    }, { passive: true });
    root.addEventListener('touchend', innerReset);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/product-image-zoom';
    credit.target = '_blank';
    credit.rel = 'noopener';
    root.appendChild(credit);

    if (script.parentNode && script.parentNode.nodeName !== 'HEAD') {
      script.insertAdjacentElement('afterend', root);
    } else {
      document.body.appendChild(root);
    }
  }

  function init() {
    var script = findScript();
    if (!script) {
      console.warn('[haynes lab product-image-zoom] Could not locate its own script tag. Nothing rendered.');
      return;
    }
    build(script);
  }

  if (document.body) {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }
})();

</script>

Configuration

Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.

AttributeDefaultDescription
data-hl-src(required)URL of the product image to display and zoom into.
data-hl-alt""Accessible alt text for the product image.
data-hl-zoom"2.5"Magnification level applied on hover; must be greater than 1.
data-hl-mode"lens"Set to "lens" for a magnifier lens or "inner" for an inner-zoom pan effect.

Frequently asked questions

How do I install the Product Image Zoom?

Paste the script tag where you want the image to appear on your page, set data-hl-src to your image URL, and the zoom viewer renders inline right at that spot.

Can I change the zoom level or effect?

Yes. Use data-hl-zoom to set the magnification factor (default 2.5) and data-hl-mode to choose between "lens" (a magnifier glass that follows the cursor) and "inner" (the image scales and pans inside its frame). Set data-hl-alt for accessible alt text.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it is a single vanilla JS file with no dependencies, so it works on WordPress, Shopify, Squarespace, or any site that lets you embed a script tag.

More free widgets

Need a custom widget?

We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.

Get in Touch