Carousel Image Block

I updated this plugin with new version, with more better options, you can check it here

Contact me if you need support.

Description: Carousel Image Block

  • set different number of images on Desktop – Mobile
  • use custom arrow icon
  • enable Lightbox on image click
  • enable navigation to other images inside lightbox
  • set different image width

Demo 1.

Plugin02v2 1 min

Demo 2.

Plugin02v2 2 min

Demo 3.

Plugin02v2 3 min

Demo 4.

Plugin02v2 4 min

#1. Install Plugin

#1.1. First, hover on Page where you want to add Carousel > Click Gear icon

Plugin02v2 5 min

#1.2. Click Advanced > Paste this code

<!-- Carousel Block @tuanphan -->
<style>
:root {
  --columns-mobile: 3;
  --columns-desktop: 6;
  --img-height-mobile: 200px;
  --img-height-desktop: 250px;
  --img-gap: 10px;
  --img-round: 0px;
  --nav-bg: rgba(255, 255, 255, 0.8);
  --nav-bg-hover: rgba(255, 255, 255, 1);
  --nav-width: 40px;
  --nav-height: 40px;
  --nav-round: 50%;
  --nav-font-size: 1.2rem;
  --nav-box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  --nav-space: 0.5rem;
}
div.markdown-block:has(.tp-markdown-wrapper) .sqs-block-content{display:none!important}.tp-markdown-wrapper{display:flex;justify-content:center}.tp-markdown-carousel-container{position:relative;width:100%;overflow:hidden}.tp-markdown-carousel-viewport{overflow:hidden;width:100%;position:relative}.tp-markdown-carousel-track{display:flex;gap:var(--img-gap);transition:transform .5s ease}.tp-markdown-carousel-track img{flex-shrink:0;object-fit:cover;height:var(--img-height-mobile);border-radius:var(--img-round);transition:transform .3s ease}.tp-markdown-carousel-track img:hover{transform:scale(1.05)}.tp-markdown-nav{position:absolute;top:50%;transform:translateY(-50%);background-color:var(--nav-bg);border:none;font-size:var(--nav-font-size);cursor:pointer;z-index:1;padding:var(--nav-space);border-radius:var(--nav-round);width:var(--nav-width);height:var(--nav-height);display:flex;align-items:center;justify-content:center;transition:background-color .3s ease;box-shadow:var(--nav-box-shadow)}.tp-markdown-nav:hover{background-color:var(--nav-bg-hover)}.tp-markdown-nav.tp-left{left:10px}.tp-markdown-nav.tp-right{right:10px}@media(max-width:767px){.tp-markdown-carousel-track img{width:calc((100% - (var(--img-gap)*(var(--columns-mobile) - 1)))/var(--columns-mobile));height:var(--img-height-mobile)}}@media(min-width:768px){.tp-markdown-carousel-track img{width:calc((100% - (var(--img-gap)*(var(--columns-desktop) - 1)))/var(--columns-desktop));height:var(--img-height-desktop)}}</style>

<script>window.MarkdownCarousel={hooks:{beforeInit:[],afterInit:[],beforeCarouselCreate:[],afterCarouselCreate:[],beforeSlide:[],afterSlide:[],onResize:[]},addHook:function(e,t){this.hooks[e]&&this.hooks[e].push(t)},removeHook:function(e,t){if(this.hooks[e]){const n=this.hooks[e].indexOf(t);n>-1&&this.hooks[e].splice(n,1)}},runHooks:function(e,t){this.hooks[e]&&this.hooks[e].forEach((e=>{try{e(t)}catch(e){console.error("Hook error:",e)}}))},getItemsPerSlide:function(){const e=getComputedStyle(document.documentElement),t=parseInt(e.getPropertyValue("--columns-mobile"))||1,n=parseInt(e.getPropertyValue("--columns-desktop"))||1;return window.innerWidth<=767?t:n},init:function(){this.runHooks("beforeInit");const e=document.querySelectorAll(".markdown-block");e.forEach((e=>{if(e.querySelector(".tp-markdown-wrapper"))return;const t=e.querySelector(".sqs-block-content");if(!t)return;const n=t.querySelectorAll('a[href*=".jpg"], a[href*=".jpeg"], a[href*=".png"], a[href*=".webp"], a[href*=".gif"]');if(0===n.length)return;const a=Array.from(n).map((e=>e.href)).filter((e=>e.match(/\.(jpg|jpeg|png|webp|gif)(\?.*)?$/i)));if(0===a.length)return;this.runHooks("beforeCarouselCreate",{markdownBlock:e,imageUrls:a});const r=`<div class="tp-markdown-wrapper"><div class="tp-markdown-carousel-container"><button class="tp-markdown-nav tp-left">&#10094;</button><div class="tp-markdown-carousel-viewport"><div class="tp-markdown-carousel-track">${a.map((e=>`<img src="${e}" alt=""/>`)).join("")}</div></div><button class="tp-markdown-nav tp-right">&#10095;</button></div></div>`;e.insertAdjacentHTML("beforeend",r);const o=this.setupCarousel(e);this.runHooks("afterCarouselCreate",{markdownBlock:e,carouselData:o,imageUrls:a})})),this.runHooks("afterInit")},setupCarousel:function(e){const t=e.querySelector(".tp-markdown-carousel-track"),n=e.querySelector(".tp-markdown-nav.tp-left"),a=e.querySelector(".tp-markdown-nav.tp-right"),r=t.querySelectorAll("img");let o=0;const i=this,l={track:t,leftBtn:n,rightBtn:a,images:r,currentIndex:()=>o,setCurrentIndex:e=>{o=e}};function c(){const e=i.getItemsPerSlide(),n=t.parentElement.offsetWidth,a=getComputedStyle(document.documentElement),c=parseInt(a.getPropertyValue("--img-gap"))||10,s=(n-c*(e-1))/e;r.forEach((e=>{e.style.width=s+"px"}));const d=s+c;t.style.transform=`translateX(-${o*d}px)`,i.runHooks("onResize",{carouselData:l,currentIndex:o,itemsPerSlide:e})}function s(){const e=i.getItemsPerSlide();return Math.max(0,r.length-e)}n.addEventListener("click",(()=>{i.runHooks("beforeSlide",{carouselData:l,direction:"left",currentIndex:o}),o--,o<0&&(o=s()),c(),i.runHooks("afterSlide",{carouselData:l,direction:"left",currentIndex:o})})),a.addEventListener("click",(()=>{i.runHooks("beforeSlide",{carouselData:l,direction:"right",currentIndex:o});const e=s();o++,o>e&&(o=0),c(),i.runHooks("afterSlide",{carouselData:l,direction:"right",currentIndex:o})})),window.addEventListener("resize",c),setTimeout(c,100);let d=new MutationObserver(c);return d.observe(t.parentElement,{attributes:!0,childList:!0,subtree:!0}),l}},"loading"===document.readyState?document.addEventListener("DOMContentLoaded",(()=>window.MarkdownCarousel.init())):window.MarkdownCarousel.init(),window.addEventListener("resize",(()=>window.MarkdownCarousel.init()));</script>
<!-- END Carousel Block @tuanphan -->

Plugin02v2 6 min

#1.3. Edit page where you want to add Carousel > Add a Markdown Block

Plugin02v2 7 min

#1.4. Paste all Image URLs

Plugin02v2 8 min

#2. Customize

#2.1. Set number of images on Desktop – Mobile

--columns-mobile: 3;
 --columns-desktop: 6;

Plugin02v2 9 min

#2.2. Set image size on desktop – mobile

--img-height-mobile: 200px;
--img-height-desktop: 250px;

Plugin02v2 10 min

#2.3. Set space between image – round corners of Images.

--img-gap: 10px;
--img-round: 0px;

Plugin02v2 11 min

#2.4. Set Navigation Arrows style

--nav-bg: rgba(255, 255, 255, 0.8);
  --nav-bg-hover: rgba(255, 255, 255, 1);
  --nav-width: 40px;
  --nav-height: 40px;
  --nav-round: 50%;
  --nav-font-size: 1.2rem;
  --nav-box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  --nav-space: 0.5rem;

Plugin02v2 12 min

#2.5. Set arrow to custom icon

Use this code to Custom CSS

button.tp-markdown-nav {
    background-color: transparent !important;
    background-size: contain !important;
    background-repeat: no-repeat;
    background-position: center;
    color: transparent !important;
}
button.tp-markdown-nav.tp-left {
    background-image: url(https://content.invisioncic.com/p289038/monthly_2022_09/[email protected]);
}
button.tp-markdown-nav.tp-right {
    background-image: url(https://content.invisioncic.com/p289038/monthly_2022_09/[email protected]);
}

Plugin02v2 13 min

To set arrows in lightbox to custom icon, use this to Custom CSS

.tp-lightbox-nav { 
    background-color: transparent !important; 
    background-size: contain !important;
    background-repeat: no-repeat; 
    background-position: center; 
    color: transparent !important; 
    border: none !important;
} 
.tp-lightbox-nav.prev { 
    background-image: url(https://content.invisioncic.com/p289038/monthly_2022_09/[email protected]); 
} 
.tp-lightbox-nav.next { 
    background-image: url(https://content.invisioncic.com/p289038/monthly_2022_09/[email protected]); 
}

 

#2.6. Set different image width

Use this code to Custom CSS

div.tp-markdown-carousel-track img {
    width: auto !important;
}

Plugin02v2 14 min

#2.7. Enable Lightbox

Use this under plugin code

<style>
.tp-lightbox-overlay {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.95);
  z-index: 99999;
  opacity: 0;
  transition: opacity 0.3s ease;
}

.tp-lightbox-overlay.active {
  display: flex;
  justify-content: center;
  align-items: center;
  opacity: 1;
}

.tp-lightbox-container {
  position: relative;
  max-width: 90%;
  max-height: 90%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.tp-lightbox-image {
  max-width: 100%;
  max-height: 90vh;
  object-fit: contain;
  cursor: default;
  transform: scale(0.9);
  transition: transform 0.3s ease;
}

.tp-lightbox-overlay.active .tp-lightbox-image {
  transform: scale(1);
}

.tp-lightbox-close {
  position: absolute;
  top: 20px;
  right: 40px;
  color: white;
  font-size: 40px;
  font-weight: 300;
  cursor: pointer;
  background: none;
  border: none;
  transition: transform 0.3s ease;
  z-index: 100000;
  line-height: 1;
  padding: 0;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.tp-lightbox-close:hover {
  transform: scale(1.2);
}

.tp-lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(255, 255, 255, 0.1);
  border: 2px solid rgba(255, 255, 255, 0.3);
  color: white;
  font-size: 30px;
  cursor: pointer;
  padding: 15px 20px;
  border-radius: 50%;
  transition: all 0.3s ease;
  z-index: 100000;
  width: 60px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.tp-lightbox-nav:hover {
  background: rgba(255, 255, 255, 0.2);
  border-color: rgba(255, 255, 255, 0.5);
}

.tp-lightbox-nav.prev {
  left: 30px;
}

.tp-lightbox-nav.next {
  right: 30px;
}

.tp-lightbox-counter {
  position: absolute;
  bottom: 30px;
  left: 50%;
  transform: translateX(-50%);
  color: white;
  font-size: 16px;
  background: rgba(0, 0, 0, 0.5);
  padding: 8px 20px;
  border-radius: 20px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

.tp-markdown-carousel-track img {
  cursor: zoom-in;
}

@media (max-width: 767px) {
  .tp-lightbox-nav {
    width: 50px;
    height: 50px;
    font-size: 24px;
  }
  
  .tp-lightbox-nav.prev {
    left: 10px;
  }
  
  .tp-lightbox-nav.next {
    right: 10px;
  }
  
  .tp-lightbox-close {
    top: 10px;
    right: 20px;
    font-size: 30px;
  }
}
</style>

<script>
(function() {
  let lightboxOverlay = null;
  let currentLightboxIndex = 0;
  let allImages = [];

  function createLightbox() {
    if (document.getElementById('tp-lightbox')) return;
    
    const lightboxHTML = `
      <div id="tp-lightbox" class="tp-lightbox-overlay">
        <button class="tp-lightbox-close">&times;</button>
        <button class="tp-lightbox-nav prev">&#10094;</button>
        <div class="tp-lightbox-container">
          <img class="tp-lightbox-image" src="" alt="">
        </div>
        <button class="tp-lightbox-nav next">&#10095;</button>
        <div class="tp-lightbox-counter">
          <span class="current">1</span> / <span class="total">1</span>
        </div>
      </div>
    `;
    
    document.body.insertAdjacentHTML('beforeend', lightboxHTML);
    
    lightboxOverlay = document.getElementById('tp-lightbox');
    const closeBtn = lightboxOverlay.querySelector('.tp-lightbox-close');
    const prevBtn = lightboxOverlay.querySelector('.tp-lightbox-nav.prev');
    const nextBtn = lightboxOverlay.querySelector('.tp-lightbox-nav.next');
    const lightboxImage = lightboxOverlay.querySelector('.tp-lightbox-image');
    
    closeBtn.addEventListener('click', closeLightbox);
    
    lightboxOverlay.addEventListener('click', function(e) {
      if (e.target === lightboxOverlay || e.target.classList.contains('tp-lightbox-container')) {
        closeLightbox();
      }
    });
    
    prevBtn.addEventListener('click', function(e) {
      e.stopPropagation();
      navigateLightbox(-1);
    });
    
    nextBtn.addEventListener('click', function(e) {
      e.stopPropagation();
      navigateLightbox(1);
    });
    
    document.addEventListener('keydown', function(e) {
      if (!lightboxOverlay.classList.contains('active')) return;
      
      switch(e.key) {
        case 'Escape':
          closeLightbox();
          break;
        case 'ArrowLeft':
          navigateLightbox(-1);
          break;
        case 'ArrowRight':
          navigateLightbox(1);
          break;
      }
    });
  }

  function openLightbox(imageSrc, index, images) {
    if (!lightboxOverlay) createLightbox();
    
    currentLightboxIndex = index;
    allImages = images;
    
    const lightboxImage = lightboxOverlay.querySelector('.tp-lightbox-image');
    const currentSpan = lightboxOverlay.querySelector('.current');
    const totalSpan = lightboxOverlay.querySelector('.total');
    
    lightboxImage.src = imageSrc;
    currentSpan.textContent = index + 1;
    totalSpan.textContent = images.length;
    
    setTimeout(() => {
      lightboxOverlay.classList.add('active');
    }, 10);
    
    document.body.style.overflow = 'hidden';
  }

  function closeLightbox() {
    lightboxOverlay.classList.remove('active');
    document.body.style.overflow = '';
  }

  function navigateLightbox(direction) {
    currentLightboxIndex += direction;
    
    if (currentLightboxIndex < 0) {
      currentLightboxIndex = allImages.length - 1;
    } else if (currentLightboxIndex >= allImages.length) {
      currentLightboxIndex = 0;
    }
    
    const lightboxImage = lightboxOverlay.querySelector('.tp-lightbox-image');
    const currentSpan = lightboxOverlay.querySelector('.current');
    
    lightboxImage.style.opacity = '0';
    lightboxImage.style.transform = 'scale(0.9)';
    
    setTimeout(() => {
      lightboxImage.src = allImages[currentLightboxIndex];
      currentSpan.textContent = currentLightboxIndex + 1;
      
      setTimeout(() => {
        lightboxImage.style.opacity = '1';
        lightboxImage.style.transform = 'scale(1)';
      }, 50);
    }, 200);
  }

  if (window.MarkdownCarousel) {
    window.MarkdownCarousel.addHook('afterCarouselCreate', function(data) {
      const { markdownBlock, imageUrls } = data;
      const images = markdownBlock.querySelectorAll('.tp-markdown-carousel-track img');
      
      images.forEach((img, index) => {
        img.addEventListener('click', function(e) {
          e.preventDefault();
          openLightbox(img.src, index, imageUrls);
        });
      });
    });
  }

  createLightbox();
})();
</script>

Plugin02v2 15 min

Result

Plugin02v2 16 min

#2.8. To enable autoscroll, use this code under plugin code

<!-- Carousel autoscroll @tuanphan -->
<script>
(function() {
  const autoScrollInterval = 3000;
  const carouselTimers = new Map();
  
  function startAutoScroll(carouselData) {
    const timer = setInterval(() => {
      if (carouselData.rightBtn) {
        carouselData.rightBtn.click();
      }
    }, autoScrollInterval);
    
    carouselTimers.set(carouselData, timer);
  }
  
  function stopAutoScroll(carouselData) {
    const timer = carouselTimers.get(carouselData);
    if (timer) {
      clearInterval(timer);
      carouselTimers.delete(carouselData);
    }
  }
  
  function restartAutoScroll(carouselData) {
    stopAutoScroll(carouselData);
    startAutoScroll(carouselData);
  }
  
  window.MarkdownCarousel.addHook('afterCarouselCreate', function(data) {
    const { markdownBlock, carouselData } = data;
    
    startAutoScroll(carouselData);
    
    const leftBtn = carouselData.leftBtn;
    const rightBtn = carouselData.rightBtn;
    
    if (leftBtn) {
      leftBtn.addEventListener('click', () => restartAutoScroll(carouselData));
    }
    
    if (rightBtn) {
      rightBtn.addEventListener('click', () => restartAutoScroll(carouselData));
    }
    
    const viewport = markdownBlock.querySelector('.tp-markdown-carousel-viewport');
    if (viewport) {
      viewport.addEventListener('mouseenter', () => stopAutoScroll(carouselData));
      viewport.addEventListener('mouseleave', () => startAutoScroll(carouselData));
    }
  });
})();
</script>

Plugin02v2 a min

#2.9. To add Infinite Scroll, you can use this code under plugin code

<!-- Infinite Scroll -->
<script>
MarkdownCarousel.makeInfinite = function() {
  const originalSetup = this.setupCarousel;
  
  this.setupCarousel = function(markdownBlock) {
    const track = markdownBlock.querySelector('.tp-markdown-carousel-track');
    const leftBtn = markdownBlock.querySelector('.tp-markdown-nav.tp-left');
    const rightBtn = markdownBlock.querySelector('.tp-markdown-nav.tp-right');
    const originalImages = Array.from(track.querySelectorAll('img'));
    
    const itemsPerSlide = this.getItemsPerSlide();
    const clonesBefore = originalImages.slice(-itemsPerSlide).map(img => {
      const clone = img.cloneNode(true);
      clone.classList.add('tp-clone-before');
      return clone;
    });
    
    const clonesAfter = originalImages.slice(0, itemsPerSlide).map(img => {
      const clone = img.cloneNode(true);
      clone.classList.add('tp-clone-after');
      return clone;
    });
    
    clonesBefore.reverse().forEach(clone => track.insertBefore(clone, track.firstChild));
    clonesAfter.forEach(clone => track.appendChild(clone));
    
    const allImages = track.querySelectorAll('img');
    let currentIndex = itemsPerSlide;
    let isTransitioning = false;
    
    const self = this;
    const carouselData = {
      track,
      leftBtn,
      rightBtn,
      images: allImages,
      currentIndex: () => currentIndex,
      setCurrentIndex: (index) => { currentIndex = index; }
    };
    
    function updateCarousel(withTransition = true) {
      const itemsPerSlide = self.getItemsPerSlide();
      const containerWidth = track.parentElement.offsetWidth;
      const style = getComputedStyle(document.documentElement);
      const gap = parseInt(style.getPropertyValue('--img-gap')) || 10;
      
      const imageWidth = (containerWidth - (gap * (itemsPerSlide - 1))) / itemsPerSlide;
      
      allImages.forEach(img => {
        img.style.width = imageWidth + 'px';
      });
      
      const moveDistance = imageWidth + gap;
      
      if (!withTransition) {
        track.style.transition = 'none';
      } else {
        track.style.transition = 'transform 0.3s ease-in-out';
      }
      
      track.style.transform = `translateX(-${currentIndex * moveDistance}px)`;
      
      if (!withTransition) {
        track.offsetHeight; 
        track.style.transition = 'transform 0.3s ease-in-out';
      }
      
      self.runHooks('onResize', { carouselData, currentIndex, itemsPerSlide });
    }
    
    function handleTransitionEnd() {
      if (isTransitioning) {
        const itemsPerSlide = self.getItemsPerSlide();
        
        if (currentIndex <= 0) {
          currentIndex = originalImages.length;
          updateCarousel(false);
        } else if (currentIndex >= originalImages.length + itemsPerSlide) {
          currentIndex = itemsPerSlide;
          updateCarousel(false);
        }
        
        isTransitioning = false;
      }
    }
    
    track.addEventListener('transitionend', handleTransitionEnd);
    
    leftBtn.addEventListener('click', () => {
      if (isTransitioning) return;
      isTransitioning = true;
      
      self.runHooks('beforeSlide', { carouselData, direction: 'left', currentIndex });
      currentIndex--;
      updateCarousel(true);
      self.runHooks('afterSlide', { carouselData, direction: 'left', currentIndex });
    });
    
    rightBtn.addEventListener('click', () => {
      if (isTransitioning) return;
      isTransitioning = true;
      
      self.runHooks('beforeSlide', { carouselData, direction: 'right', currentIndex });
      currentIndex++;
      updateCarousel(true);
      self.runHooks('afterSlide', { carouselData, direction: 'right', currentIndex });
    });
    
    window.addEventListener('resize', () => updateCarousel(false));
    
    setTimeout(() => updateCarousel(false), 100);
    
    return carouselData;
  };
};
MarkdownCarousel.makeInfinite();
</script>

 

Buy me a coffee