Content & Media

Free Image Carousel Widget

Responsive image slideshow with arrows, dots, autoplay, and swipe support.

Image Carousel turns a simple JSON list of images into a responsive slideshow with arrow buttons, dot navigation, optional autoplay, and touch swipe support. Each slide can link to a URL, making it perfect for product shots, promos, and featured content. Install is a single script tag on WordPress, Shopify, or any site.

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/image-carousel.js" data-hl-config='[{"src":"https://placehold.co/720x360/f97316/ffffff?text=Slide+1","alt":"Orange placeholder slide"},{"src":"https://placehold.co/720x360/06b6d4/ffffff?text=Slide+2","alt":"Cyan placeholder slide"},{"src":"https://placehold.co/720x360/111827/ffffff?text=Slide+3","alt":"Dark placeholder slide","href":"https://hayneslab.dev"},{"src":"https://placehold.co/720x360/8b5cf6/ffffff?text=Slide+4","alt":"Purple placeholder slide"}]' data-hl-autoplay="true" async></script>
<!-- Free Image Carousel widget by haynes lab — https://hayneslab.dev/tools/widgets/image-carousel -->
Inline (self-contained)html
<!-- Free Image Carousel widget by haynes lab — https://hayneslab.dev/tools/widgets/image-carousel -->
<script data-hl-config='[{"src":"https://placehold.co/720x360/f97316/ffffff?text=Slide+1","alt":"Orange placeholder slide"},{"src":"https://placehold.co/720x360/06b6d4/ffffff?text=Slide+2","alt":"Cyan placeholder slide"},{"src":"https://placehold.co/720x360/111827/ffffff?text=Slide+3","alt":"Dark placeholder slide","href":"https://hayneslab.dev"},{"src":"https://placehold.co/720x360/8b5cf6/ffffff?text=Slide+4","alt":"Purple placeholder slide"}]' data-hl-autoplay="true">
/*!
 * haynes lab — Image Carousel widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/image-carousel
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-image-carousel';
  var STYLE_ID = 'hlw-image-carousel-styles';

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

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; max-width: 720px; margin: 0 auto; overflow: hidden;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 16px; line-height: 1.5; color: #1f2937; background: #f3f4f6;' +
      ' border: 1px solid #e5e7eb; border-radius: 12px; touch-action: pan-y; }' +
      '.' + PREFIX + '__track { display: flex; transition: transform 0.45s ease; }' +
      '.' + PREFIX + '__slide { flex: 0 0 100%; min-width: 0; display: block; }' +
      '.' + PREFIX + '__img { display: block; width: 100%; height: auto; }' +
      '.' + PREFIX + '__arrow { position: absolute; top: 50%; transform: translateY(-50%); width: 36px; height: 36px;' +
      ' border: 0; border-radius: 50%; background: rgba(255, 255, 255, 0.9); color: #374151; font-size: 18px;' +
      ' line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0;' +
      ' box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); }' +
      '.' + PREFIX + '__arrow:hover { color: var(--hlw-accent); }' +
      '.' + PREFIX + '__arrow:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__arrow--prev { left: 10px; }' +
      '.' + PREFIX + '__arrow--next { right: 10px; }' +
      '.' + PREFIX + '__dots { position: absolute; left: 0; right: 0; bottom: 10px; display: flex; gap: 8px; justify-content: center; }' +
      '.' + PREFIX + '__dot { width: 9px; height: 9px; border-radius: 50%; border: 0; padding: 0;' +
      ' background: rgba(255, 255, 255, 0.6); cursor: pointer; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); }' +
      '.' + PREFIX + '__dot[aria-current="true"] { background: var(--hlw-accent); }' +
      '.' + PREFIX + '__dot:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__credit { position: absolute; right: 10px; bottom: 8px; font-size: 11px;' +
      ' display: inline-flex; align-items: center; gap: 4px; color: rgba(255, 255, 255, 0.85);' +
      ' text-decoration: none; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #ffffff; text-decoration: underline; }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function parseSlides(script) {
    var raw = script.getAttribute('data-hl-config');
    if (!raw) {
      console.warn('[haynes lab image-carousel] Missing required data-hl-config attribute (JSON array of {src, alt, href?}). Nothing rendered.');
      return null;
    }
    var items;
    try {
      items = JSON.parse(raw);
    } catch (e) {
      console.warn('[haynes lab image-carousel] data-hl-config is not valid JSON. Nothing rendered.', e);
      return null;
    }
    if (!Array.isArray(items)) {
      console.warn('[haynes lab image-carousel] data-hl-config must be a JSON array. Nothing rendered.');
      return null;
    }
    var valid = items.filter(function (s) {
      return s && typeof s.src === 'string' && s.src;
    });
    if (!valid.length) {
      console.warn('[haynes lab image-carousel] data-hl-config has no valid slides (each needs at least "src"). Nothing rendered.');
      return null;
    }
    return valid;
  }

  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 build(script) {
    var items = parseSlides(script);
    if (!items) return;

    var autoplay = script.getAttribute('data-hl-autoplay') === 'true';
    var interval = parseInt(script.getAttribute('data-hl-interval'), 10);
    if (isNaN(interval) || interval < 2000) interval = 5000;
    var accent = script.getAttribute('data-hl-accent') || '#f97316';

    injectStyles();

    var root = el('div', PREFIX);
    root.setAttribute('role', 'region');
    root.setAttribute('aria-roledescription', 'carousel');
    root.setAttribute('aria-label', 'Image carousel');
    root.style.setProperty('--hlw-accent', accent);

    var track = el('div', PREFIX + '__track');
    root.appendChild(track);

    items.forEach(function (s, i) {
      var img = el('img', PREFIX + '__img');
      img.src = s.src;
      img.alt = typeof s.alt === 'string' ? s.alt : '';
      img.loading = i === 0 ? 'eager' : 'lazy';
      var slide;
      if (typeof s.href === 'string' && s.href) {
        slide = el('a', PREFIX + '__slide');
        slide.href = s.href;
      } else {
        slide = el('div', PREFIX + '__slide');
      }
      slide.setAttribute('aria-roledescription', 'slide');
      slide.setAttribute('aria-label', (i + 1) + ' of ' + items.length);
      slide.appendChild(img);
      track.appendChild(slide);
    });

    var dots = el('div', PREFIX + '__dots');
    var index = 0;
    var timer = null;
    var touchX = null;

    function go(i) {
      index = (i + items.length) % items.length;
      track.style.transform = 'translateX(-' + index * 100 + '%)';
      Array.prototype.forEach.call(dots.children, function (d, di) {
        d.setAttribute('aria-current', di === index ? 'true' : 'false');
      });
    }

    function stop() {
      if (timer) { clearInterval(timer); timer = null; }
    }
    function start() {
      if (autoplay && items.length > 1 && !timer) {
        timer = setInterval(function () { go(index + 1); }, interval);
      }
    }

    if (items.length > 1) {
      var prev = el('button', PREFIX + '__arrow ' + PREFIX + '__arrow--prev', '‹');
      prev.type = 'button';
      prev.setAttribute('aria-label', 'Previous slide');
      var next = el('button', PREFIX + '__arrow ' + PREFIX + '__arrow--next', '›');
      next.type = 'button';
      next.setAttribute('aria-label', 'Next slide');
      prev.addEventListener('click', function () { stop(); go(index - 1); start(); });
      next.addEventListener('click', function () { stop(); go(index + 1); start(); });
      root.appendChild(prev);
      root.appendChild(next);

      items.forEach(function (_, di) {
        var dot = el('button', PREFIX + '__dot');
        dot.type = 'button';
        dot.setAttribute('aria-label', 'Go to slide ' + (di + 1));
        dot.setAttribute('aria-current', di === 0 ? 'true' : 'false');
        dot.addEventListener('click', function () { stop(); go(di); start(); });
        dots.appendChild(dot);
      });
      root.appendChild(dots);

      root.addEventListener('touchstart', function (e) {
        touchX = e.touches[0].clientX;
        stop();
      }, { passive: true });
      root.addEventListener('touchend', function (e) {
        if (touchX == null) return;
        var dx = e.changedTouches[0].clientX - touchX;
        touchX = null;
        if (dx <= -40) go(index + 1);
        else if (dx >= 40) go(index - 1);
        start();
      }, { passive: true });

      root.addEventListener('mouseenter', stop);
      root.addEventListener('mouseleave', start);
      root.addEventListener('focusin', stop);
      root.addEventListener('focusout', start);
      start();
    }

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/image-carousel';
    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 image-carousel] 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-config(required)JSON array of slide objects, each with "src" and "alt", plus an optional "href" to make the slide a link.
data-hl-autoplay"false"Set to "true" to advance slides automatically; pauses on hover, focus, and touch.
data-hl-interval"5000"Milliseconds between slides when autoplay is on (minimum 2000).
data-hl-accent"#f97316"Color of the active dot and hover/focus states on the arrows.

Frequently asked questions

How do I install the Image Carousel?

Paste the script tag with a data-hl-config attribute containing your slides as a JSON array of {src, alt, href?} objects, right where you want the carousel to appear. The slideshow renders inline at that spot in your page.

Can I control autoplay, speed, and colors?

Yes. Set data-hl-autoplay="true" to auto-advance slides and data-hl-interval to the delay in milliseconds. Use data-hl-accent to change the active dot and arrow hover color. Autoplay pauses on hover, keyboard focus, and touch.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it is a single vanilla JS file with no dependencies. Paste the script tag into any HTML block, embed element, or code injection area and it works.

Does it work on mobile and with linked slides?

Yes. The carousel supports touch swiping out of the box and scales to its container width. Add an "href" to any slide object to wrap that image in a link.

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