chunk-039-carousel.js

I’ll build this as a standalone advanced vanilla JavaScript file for your RX Theme carousel chunk, so it can work without jQuery and load safely only when carousel markup exists.

Dear friend, create this file:

assets/static-js/static-chunks-js/chunk-039-carousel.js

It is an advanced vanilla JS carousel chunk for RX Theme: autoplay, pause on hover, swipe/touch, keyboard, dots, arrows, infinite loop, lazy image loading, progress bar, reduced-motion support, accessibility, RTL support, multiple carousel support, responsive visible slides, and safe WordPress theme loading.

/*!
 * RX Theme - Advanced Carousel Chunk
 * File: assets/static-js/static-chunks-js/chunk-039-carousel.js
 * Author: RxHarun
 * Description: Advanced accessible responsive carousel for RX Theme.
 */

(function () {
  'use strict';

  const RX_CAROUSEL_VERSION = '1.0.0';

  const DEFAULTS = {
    selector: '[data-rx-carousel]',
    trackSelector: '[data-rx-carousel-track]',
    slideSelector: '[data-rx-carousel-slide]',
    prevSelector: '[data-rx-carousel-prev]',
    nextSelector: '[data-rx-carousel-next]',
    dotsSelector: '[data-rx-carousel-dots]',
    progressSelector: '[data-rx-carousel-progress]',

    autoplay: true,
    autoplayDelay: 5000,
    autoplayOnMobile: true,
    pauseOnHover: true,
    pauseOnFocus: true,
    pauseWhenHidden: true,

    loop: true,
    keyboard: true,
    swipe: true,
    mouseDrag: false,
    rtl: false,

    transitionSpeed: 450,
    easing: 'ease',
    startIndex: 0,

    slidesPerView: 1,
    slidesToScroll: 1,
    gap: 16,

    lazyLoad: true,
    adaptiveHeight: false,
    announce: true,
    reducedMotionRespect: true,

    breakpoints: {
      480: {
        slidesPerView: 1,
        gap: 12
      },
      768: {
        slidesPerView: 2,
        gap: 16
      },
      1024: {
        slidesPerView: 3,
        gap: 20
      },
      1280: {
        slidesPerView: 4,
        gap: 24
      }
    }
  };

  const stateMap = new WeakMap();

  const prefersReducedMotion = window.matchMedia &&
    window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  function toBool(value, fallback) {
    if (value === undefined || value === null || value === '') return fallback;
    if (value === 'true' || value === true) return true;
    if (value === 'false' || value === false) return false;
    return fallback;
  }

  function toNumber(value, fallback) {
    const number = Number(value);
    return Number.isFinite(number) ? number : fallback;
  }

  function clamp(number, min, max) {
    return Math.max(min, Math.min(number, max));
  }

  function mergeOptions(base, custom) {
    const output = Object.assign({}, base, custom);

    output.breakpoints = Object.assign(
      {},
      base.breakpoints || {},
      custom.breakpoints || {}
    );

    return output;
  }

  function getDatasetOptions(root) {
    return {
      autoplay: toBool(root.dataset.rxCarouselAutoplay, DEFAULTS.autoplay),
      autoplayDelay: toNumber(root.dataset.rxCarouselDelay, DEFAULTS.autoplayDelay),
      loop: toBool(root.dataset.rxCarouselLoop, DEFAULTS.loop),
      keyboard: toBool(root.dataset.rxCarouselKeyboard, DEFAULTS.keyboard),
      swipe: toBool(root.dataset.rxCarouselSwipe, DEFAULTS.swipe),
      mouseDrag: toBool(root.dataset.rxCarouselMouseDrag, DEFAULTS.mouseDrag),
      slidesPerView: toNumber(root.dataset.rxCarouselView, DEFAULTS.slidesPerView),
      slidesToScroll: toNumber(root.dataset.rxCarouselScroll, DEFAULTS.slidesToScroll),
      gap: toNumber(root.dataset.rxCarouselGap, DEFAULTS.gap),
      adaptiveHeight: toBool(root.dataset.rxCarouselAdaptiveHeight, DEFAULTS.adaptiveHeight),
      startIndex: toNumber(root.dataset.rxCarouselStart, DEFAULTS.startIndex),
      rtl: toBool(root.dataset.rxCarouselRtl, document.dir === 'rtl')
    };
  }

  function getResponsiveOptions(options) {
    const width = window.innerWidth || document.documentElement.clientWidth;
    let responsive = {};

    Object.keys(options.breakpoints || {})
      .map(Number)
      .sort((a, b) => a - b)
      .forEach(function (breakpoint) {
        if (width >= breakpoint) {
          responsive = Object.assign(responsive, options.breakpoints[breakpoint]);
        }
      });

    return Object.assign({}, options, responsive);
  }

  function createLiveRegion(root) {
    let live = root.querySelector('[data-rx-carousel-live]');

    if (!live) {
      live = document.createElement('div');
      live.setAttribute('data-rx-carousel-live', '');
      live.setAttribute('aria-live', 'polite');
      live.setAttribute('aria-atomic', 'true');
      live.className = 'rx-carousel__live screen-reader-text';
      root.appendChild(live);
    }

    return live;
  }

  function setupAccessibility(instance) {
    const { root, track, slides, options } = instance;

    root.setAttribute('role', 'region');
    root.setAttribute('aria-roledescription', 'carousel');

    if (!root.getAttribute('aria-label')) {
      root.setAttribute('aria-label', 'RX content carousel');
    }

    track.setAttribute('aria-live', options.autoplay ? 'off' : 'polite');

    slides.forEach(function (slide, index) {
      slide.setAttribute('role', 'group');
      slide.setAttribute('aria-roledescription', 'slide');
      slide.setAttribute('aria-label', `${index + 1} of ${slides.length}`);
      slide.setAttribute('data-rx-carousel-index', String(index));
    });

    instance.liveRegion = createLiveRegion(root);
  }

  function buildDots(instance) {
    const { dots, slides, options } = instance;

    if (!dots) return;

    dots.innerHTML = '';
    dots.setAttribute('role', 'tablist');
    dots.classList.add('rx-carousel__dots');

    const pages = getPageCount(instance);

    for (let i = 0; i < pages; i++) {
      const button = document.createElement('button');
      button.type = 'button';
      button.className = 'rx-carousel__dot';
      button.setAttribute('data-rx-carousel-dot', String(i));
      button.setAttribute('aria-label', `Go to slide ${i + 1}`);
      button.setAttribute('role', 'tab');

      button.addEventListener('click', function () {
        goTo(instance, i * options.slidesToScroll, true);
      });

      dots.appendChild(button);
    }

    instance.dotButtons = Array.from(dots.querySelectorAll('[data-rx-carousel-dot]'));
  }

  function getPageCount(instance) {
    const { slides, options } = instance;
    const maxIndex = Math.max(slides.length - options.slidesPerView, 0);

    if (maxIndex === 0) return 1;

    return Math.ceil(maxIndex / options.slidesToScroll) + 1;
  }

  function setLayout(instance) {
    const { root, track, slides, options } = instance;

    root.style.setProperty('--rx-carousel-gap', `${options.gap}px`);
    root.style.setProperty('--rx-carousel-speed', `${options.transitionSpeed}ms`);
    root.style.setProperty('--rx-carousel-easing', options.easing);
    root.style.setProperty('--rx-carousel-view', String(options.slidesPerView));

    track.style.display = 'flex';
    track.style.gap = `${options.gap}px`;
    track.style.willChange = 'transform';
    track.style.transition = `transform ${options.transitionSpeed}ms ${options.easing}`;

    const slideWidth = `calc((100% - (${options.gap}px * ${options.slidesPerView - 1})) / ${options.slidesPerView})`;

    slides.forEach(function (slide) {
      slide.style.flex = `0 0 ${slideWidth}`;
      slide.style.maxWidth = slideWidth;
    });
  }

  function updateTransform(instance, animate) {
    const { track, options } = instance;

    if (!animate) {
      track.style.transition = 'none';
    } else {
      track.style.transition = `transform ${options.transitionSpeed}ms ${options.easing}`;
    }

    const slide = instance.slides[instance.currentIndex];

    if (!slide) return;

    const direction = options.rtl ? 1 : -1;
    const offset = slide.offsetLeft;
    const translate = direction * offset;

    track.style.transform = `translate3d(${translate}px, 0, 0)`;

    if (!animate) {
      requestAnimationFrame(function () {
        track.style.transition = `transform ${options.transitionSpeed}ms ${options.easing}`;
      });
    }
  }

  function updateControls(instance) {
    const { prevButton, nextButton, options, slides, currentIndex } = instance;
    const maxIndex = Math.max(slides.length - options.slidesPerView, 0);

    if (!options.loop) {
      if (prevButton) prevButton.disabled = currentIndex <= 0;
      if (nextButton) nextButton.disabled = currentIndex >= maxIndex;
    }

    if (instance.dotButtons && instance.dotButtons.length) {
      const activePage = Math.round(currentIndex / options.slidesToScroll);

      instance.dotButtons.forEach(function (dot, index) {
        const active = index === activePage;
        dot.classList.toggle('is-active', active);
        dot.setAttribute('aria-selected', active ? 'true' : 'false');
        dot.setAttribute('tabindex', active ? '0' : '-1');
      });
    }

    slides.forEach(function (slide, index) {
      const visible =
        index >= currentIndex &&
        index < currentIndex + options.slidesPerView;

      slide.classList.toggle('is-active', index === currentIndex);
      slide.classList.toggle('is-visible', visible);
      slide.setAttribute('aria-hidden', visible ? 'false' : 'true');

      const focusable = slide.querySelectorAll(
        'a, button, input, textarea, select, details, [tabindex]'
      );

      focusable.forEach(function (item) {
        if (visible) {
          if (item.dataset.rxOldTabindex !== undefined) {
            item.setAttribute('tabindex', item.dataset.rxOldTabindex);
            delete item.dataset.rxOldTabindex;
          } else {
            item.removeAttribute('tabindex');
          }
        } else {
          if (item.hasAttribute('tabindex')) {
            item.dataset.rxOldTabindex = item.getAttribute('tabindex');
          }
          item.setAttribute('tabindex', '-1');
        }
      });
    });
  }

  function updateProgress(instance) {
    const { progress, slides, options, currentIndex } = instance;

    if (!progress) return;

    const maxIndex = Math.max(slides.length - options.slidesPerView, 0);
    const percentage = maxIndex === 0 ? 100 : ((currentIndex / maxIndex) * 100);

    progress.style.width = `${clamp(percentage, 0, 100)}%`;
    progress.setAttribute('aria-valuenow', String(Math.round(percentage)));
  }

  function updateAdaptiveHeight(instance) {
    const { root, slides, options, currentIndex } = instance;

    if (!options.adaptiveHeight) return;

    const activeSlide = slides[currentIndex];

    if (!activeSlide) return;

    root.style.height = `${activeSlide.offsetHeight}px`;
  }

  function announce(instance) {
    if (!instance.options.announce || !instance.liveRegion) return;

    instance.liveRegion.textContent =
      `Slide ${instance.currentIndex + 1} of ${instance.slides.length}`;
  }

  function lazyLoad(instance) {
    if (!instance.options.lazyLoad) return;

    const { slides, currentIndex, options } = instance;
    const preloadBefore = 1;
    const preloadAfter = options.slidesPerView + 1;

    const from = Math.max(0, currentIndex - preloadBefore);
    const to = Math.min(slides.length - 1, currentIndex + preloadAfter);

    for (let i = from; i <= to; i++) {
      const images = slides[i].querySelectorAll('img[data-src], source[data-srcset]');

      images.forEach(function (media) {
        if (media.dataset.src) {
          media.src = media.dataset.src;
          media.removeAttribute('data-src');
        }

        if (media.dataset.srcset) {
          media.srcset = media.dataset.srcset;
          media.removeAttribute('data-srcset');
        }
      });
    }
  }

  function refresh(instance, animate) {
    setLayout(instance);
    updateTransform(instance, animate);
    updateControls(instance);
    updateProgress(instance);
    updateAdaptiveHeight(instance);
    lazyLoad(instance);
    announce(instance);
  }

  function normalizeIndex(instance, targetIndex) {
    const { slides, options } = instance;
    const maxIndex = Math.max(slides.length - options.slidesPerView, 0);

    if (options.loop) {
      if (targetIndex > maxIndex) return 0;
      if (targetIndex < 0) return maxIndex;
      return targetIndex;
    }

    return clamp(targetIndex, 0, maxIndex);
  }

  function goTo(instance, index, userAction) {
    const target = normalizeIndex(instance, index);

    if (target === instance.currentIndex && userAction) {
      restartAutoplay(instance);
      return;
    }

    instance.currentIndex = target;
    refresh(instance, true);

    if (userAction) {
      restartAutoplay(instance);
    }

    instance.root.dispatchEvent(
      new CustomEvent('rxCarouselChange', {
        detail: {
          index: instance.currentIndex,
          version: RX_CAROUSEL_VERSION
        }
      })
    );
  }

  function next(instance, userAction) {
    goTo(instance, instance.currentIndex + instance.options.slidesToScroll, userAction);
  }

  function prev(instance, userAction) {
    goTo(instance, instance.currentIndex - instance.options.slidesToScroll, userAction);
  }

  function startAutoplay(instance) {
    const { options } = instance;

    if (!options.autoplay) return;
    if (!options.autoplayOnMobile && window.innerWidth < 768) return;
    if (options.reducedMotionRespect && prefersReducedMotion) return;
    if (instance.autoplayTimer) return;

    instance.autoplayTimer = window.setInterval(function () {
      if (!instance.paused) {
        next(instance, false);
      }
    }, options.autoplayDelay);

    instance.root.classList.add('is-autoplaying');
  }

  function stopAutoplay(instance) {
    if (instance.autoplayTimer) {
      window.clearInterval(instance.autoplayTimer);
      instance.autoplayTimer = null;
    }

    instance.root.classList.remove('is-autoplaying');
  }

  function restartAutoplay(instance) {
    stopAutoplay(instance);
    startAutoplay(instance);
  }

  function pause(instance) {
    instance.paused = true;
    instance.root.classList.add('is-paused');
  }

  function resume(instance) {
    instance.paused = false;
    instance.root.classList.remove('is-paused');
  }

  function bindButtons(instance) {
    const { prevButton, nextButton } = instance;

    if (prevButton) {
      prevButton.addEventListener('click', function () {
        prev(instance, true);
      });
    }

    if (nextButton) {
      nextButton.addEventListener('click', function () {
        next(instance, true);
      });
    }
  }

  function bindKeyboard(instance) {
    if (!instance.options.keyboard) return;

    instance.root.addEventListener('keydown', function (event) {
      const key = event.key;

      if (key === 'ArrowLeft') {
        event.preventDefault();
        instance.options.rtl ? next(instance, true) : prev(instance, true);
      }

      if (key === 'ArrowRight') {
        event.preventDefault();
        instance.options.rtl ? prev(instance, true) : next(instance, true);
      }

      if (key === 'Home') {
        event.preventDefault();
        goTo(instance, 0, true);
      }

      if (key === 'End') {
        event.preventDefault();
        goTo(instance, instance.slides.length - instance.options.slidesPerView, true);
      }
    });
  }

  function bindPauseEvents(instance) {
    const { root, options } = instance;

    if (options.pauseOnHover) {
      root.addEventListener('mouseenter', function () {
        pause(instance);
      });

      root.addEventListener('mouseleave', function () {
        resume(instance);
      });
    }

    if (options.pauseOnFocus) {
      root.addEventListener('focusin', function () {
        pause(instance);
      });

      root.addEventListener('focusout', function () {
        resume(instance);
      });
    }

    if (options.pauseWhenHidden) {
      document.addEventListener('visibilitychange', function () {
        if (document.hidden) {
          pause(instance);
        } else {
          resume(instance);
        }
      });
    }
  }

  function bindSwipe(instance) {
    const { root, track, options } = instance;

    if (!options.swipe) return;

    let startX = 0;
    let currentX = 0;
    let startY = 0;
    let dragging = false;
    let pointerId = null;

    function pointerDown(event) {
      if (event.pointerType === 'mouse' && !options.mouseDrag) return;

      dragging = true;
      pointerId = event.pointerId;
      startX = event.clientX;
      currentX = event.clientX;
      startY = event.clientY;

      track.style.transition = 'none';

      if (root.setPointerCapture) {
        try {
          root.setPointerCapture(pointerId);
        } catch (error) {}
      }

      pause(instance);
    }

    function pointerMove(event) {
      if (!dragging || event.pointerId !== pointerId) return;

      currentX = event.clientX;

      const diffX = currentX - startX;
      const diffY = event.clientY - startY;

      if (Math.abs(diffY) > Math.abs(diffX)) return;

      event.preventDefault();

      const baseSlide = instance.slides[instance.currentIndex];
      const baseOffset = baseSlide ? baseSlide.offsetLeft : 0;
      const direction = options.rtl ? 1 : -1;
      const translate = direction * baseOffset + diffX;

      track.style.transform = `translate3d(${translate}px, 0, 0)`;
    }

    function pointerUp(event) {
      if (!dragging || event.pointerId !== pointerId) return;

      dragging = false;

      const diff = currentX - startX;
      const threshold = Math.max(root.offsetWidth * 0.12, 45);

      track.style.transition = `transform ${options.transitionSpeed}ms ${options.easing}`;

      if (Math.abs(diff) > threshold) {
        if (diff < 0) {
          options.rtl ? prev(instance, true) : next(instance, true);
        } else {
          options.rtl ? next(instance, true) : prev(instance, true);
        }
      } else {
        refresh(instance, true);
      }

      resume(instance);
      restartAutoplay(instance);
    }

    root.addEventListener('pointerdown', pointerDown);
    root.addEventListener('pointermove', pointerMove, { passive: false });
    root.addEventListener('pointerup', pointerUp);
    root.addEventListener('pointercancel', pointerUp);
  }

  function bindResize(instance) {
    let resizeTimer = null;

    window.addEventListener('resize', function () {
      window.clearTimeout(resizeTimer);

      resizeTimer = window.setTimeout(function () {
        const newOptions = getResponsiveOptions(instance.baseOptions);

        instance.options = newOptions;
        instance.currentIndex = normalizeIndex(instance, instance.currentIndex);

        buildDots(instance);
        refresh(instance, false);
        restartAutoplay(instance);
      }, 150);
    });
  }

  function observeImages(instance) {
    const images = instance.root.querySelectorAll('img');

    images.forEach(function (image) {
      if (image.complete) return;

      image.addEventListener('load', function () {
        updateAdaptiveHeight(instance);
      });
    });
  }

  function initCarousel(root, customOptions) {
    if (!root || stateMap.has(root)) return null;

    const datasetOptions = getDatasetOptions(root);
    const baseOptions = mergeOptions(DEFAULTS, customOptions || {});
    const mergedOptions = mergeOptions(baseOptions, datasetOptions);
    const options = getResponsiveOptions(mergedOptions);

    const track = root.querySelector(options.trackSelector);
    const slides = track ? Array.from(track.querySelectorAll(options.slideSelector)) : [];

    if (!track || !slides.length) {
      root.classList.add('rx-carousel--not-ready');
      return null;
    }

    const instance = {
      root,
      track,
      slides,
      options,
      baseOptions: mergedOptions,
      currentIndex: clamp(options.startIndex, 0, Math.max(slides.length - 1, 0)),
      prevButton: root.querySelector(options.prevSelector),
      nextButton: root.querySelector(options.nextSelector),
      dots: root.querySelector(options.dotsSelector),
      progress: root.querySelector(options.progressSelector),
      dotButtons: [],
      liveRegion: null,
      autoplayTimer: null,
      paused: false
    };

    stateMap.set(root, instance);

    root.classList.add('rx-carousel');
    root.setAttribute('data-rx-carousel-version', RX_CAROUSEL_VERSION);

    setupAccessibility(instance);
    buildDots(instance);
    bindButtons(instance);
    bindKeyboard(instance);
    bindPauseEvents(instance);
    bindSwipe(instance);
    bindResize(instance);
    observeImages(instance);

    refresh(instance, false);
    startAutoplay(instance);

    root.dispatchEvent(
      new CustomEvent('rxCarouselReady', {
        detail: {
          version: RX_CAROUSEL_VERSION,
          totalSlides: slides.length
        }
      })
    );

    return instance;
  }

  function destroyCarousel(root) {
    const instance = stateMap.get(root);

    if (!instance) return;

    stopAutoplay(instance);

    instance.track.removeAttribute('style');

    instance.slides.forEach(function (slide) {
      slide.removeAttribute('style');
      slide.removeAttribute('aria-hidden');
      slide.classList.remove('is-active', 'is-visible');
    });

    root.classList.remove('rx-carousel', 'is-autoplaying', 'is-paused');

    stateMap.delete(root);
  }

  function initAll(customOptions) {
    const selector = customOptions && customOptions.selector
      ? customOptions.selector
      : DEFAULTS.selector;

    const carousels = document.querySelectorAll(selector);

    carousels.forEach(function (carousel) {
      initCarousel(carousel, customOptions || {});
    });
  }

  function ready(callback) {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', callback);
    } else {
      callback();
    }
  }

  window.RXCarousel = {
    version: RX_CAROUSEL_VERSION,
    init: initCarousel,
    initAll,
    destroy: destroyCarousel,
    next: function (root) {
      const instance = stateMap.get(root);
      if (instance) next(instance, true);
    },
    prev: function (root) {
      const instance = stateMap.get(root);
      if (instance) prev(instance, true);
    },
    goTo: function (root, index) {
      const instance = stateMap.get(root);
      if (instance) goTo(instance, index, true);
    },
    pause: function (root) {
      const instance = stateMap.get(root);
      if (instance) pause(instance);
    },
    resume: function (root) {
      const instance = stateMap.get(root);
      if (instance) resume(instance);
    }
  };

  ready(function () {
    initAll();
  });
})();

HTML structure example

Use this type of markup in your RX Theme template:

<section
  class="rx-carousel-wrap"
  data-rx-carousel
  data-rx-carousel-autoplay="true"
  data-rx-carousel-delay="5000"
  data-rx-carousel-loop="true"
  data-rx-carousel-view="1"
  data-rx-carousel-gap="16"
>
  <div class="rx-carousel__viewport">
    <div class="rx-carousel__track" data-rx-carousel-track>
      <article class="rx-carousel__slide" data-rx-carousel-slide>
        Slide 1
      </article>

      <article class="rx-carousel__slide" data-rx-carousel-slide>
        Slide 2
      </article>

      <article class="rx-carousel__slide" data-rx-carousel-slide>
        Slide 3
      </article>
    </div>
  </div>

  <button class="rx-carousel__prev" data-rx-carousel-prev type="button">
    Previous
  </button>

  <button class="rx-carousel__next" data-rx-carousel-next type="button">
    Next
  </button>

  <div class="rx-carousel__dots" data-rx-carousel-dots></div>

  <div class="rx-carousel__progress-wrap" aria-hidden="true">
    <div class="rx-carousel__progress" data-rx-carousel-progress></div>
  </div>
</section>

Minimum CSS needed

Add this in your carousel CSS file:

.rx-carousel-wrap {
  position: relative;
  overflow: hidden;
}

.rx-carousel__viewport {
  overflow: hidden;
  width: 100%;
}

.rx-carousel__track {
  display: flex;
}

.rx-carousel__slide {
  min-width: 0;
}

.rx-carousel__prev,
.rx-carousel__next,
.rx-carousel__dot {
  cursor: pointer;
}

.rx-carousel__dot.is-active {
  opacity: 1;
}

.rx-carousel__progress-wrap {
  width: 100%;
  height: 3px;
  overflow: hidden;
  background: rgba(0, 0, 0, 0.08);
}

.rx-carousel__progress {
  width: 0;
  height: 100%;
  transition: width 300ms ease;
  background: currentColor;
}

.screen-reader-text {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

Enqueue in RX Theme

In your theme enqueue file, add:

wp_enqueue_script(
    'rx-carousel-chunk',
    get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-039-carousel.js',
    array(),
    wp_get_theme()->get('Version'),
    true
);

For best performance, load it only when carousel exists on the page.

Leave a Reply

Your email address will not be published. Required fields are marked *