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.