Carousel
O UniversalIncreazyCarousel é um carrossel JavaScript altamente customizável que suporta múltiplos slides, drag, autoplay, navegação por setas, paginação e breakpoints responsivos.
Funcionalidades principais:
✅ Loop infinito ou navegação limitada
✅ Drag & swipe (mouse e touch)
✅ Autoplay configurável
✅ Navegação por setas e paginação
✅ Breakpoints responsivos
✅ Orientação horizontal ou vertical
✅ Múltiplos slides visíveis
✅ Clonagem automática para loop infinito
Código Completo
- Javascript
class UniversalIncreazyCarousel {
constructor(options) {
this.gallery = options.gallery;
this.galleryInner = options.galleryInner;
this.thumbs = options.thumbs || [];
this.prevBtn = options.prevBtn;
this.nextBtn = options.nextBtn;
this.pagination = options.pagination || null;
this.gap = options.gap ?? 0;
this.orientation = options.orientation || 'horizontal';
this.enableDrag = options.enableDrag !== false;
this.infinityLoop = options.infinityLoop ?? true;
this.slidesToScroll = options.slidesToScroll || 1;
this.slidesVisible = options.slidesVisible || 1;
this.autoplay = options.autoplay ?? false;
this.autoplayTimer = null;
this.breakpoints = options.breakpoints || null;
this.cachedDimensions = {
containerSize: 0,
innerSize: 0,
slideSize: 0,
timestamp: 0
};
if (!this.galleryInner || !this.gallery) {
console.error('Gallery ou galleryInner não encontrados');
return;
}
this.originalSlides = Array.from(this.galleryInner.children).filter(child => !child.classList.contains('clone'));
this.originalImages = Array.from(this.galleryInner.querySelectorAll('img:not(.clone)'));
this.totalImages = this.originalSlides.length;
this.currentIndex = 0;
this.startPos = 0;
this.endPos = 0;
this.isDragging = false;
this.startTranslate = 0;
this.isTransitioning = false;
this.rafId = null;
this.startTime = 0;
this.velocity = 0;
this.isDragged = false;
this.slideSize = 0;
this.maxTranslate = 0;
this.minTranslate = 0;
this.options = options;
this.applyBreakpoints();
this.init();
this.setupEventListeners();
this.setupResizeObserver();
}
calculateDimensions() {
const isHorizontal = this.orientation === 'horizontal';
const containerRect = this.galleryInner.getBoundingClientRect();
const containerSize = isHorizontal ? containerRect.width : containerRect.height;
const innerSize = isHorizontal ? this.galleryInner.scrollWidth : this.galleryInner.scrollHeight;
const gapValue = Number(this.gap) || 0;
const totalGaps = (this.slidesVisible - 1) * gapValue;
const slideSize = (containerSize - totalGaps) / this.slidesVisible;
this.cachedDimensions = {
containerSize,
innerSize,
slideSize,
timestamp: Date.now()
};
this.slideSize = slideSize;
this.innerMaxTranslate = 0;
this.innerMinTranslate = containerSize - innerSize;
const maxIndexWhenNoLoop = Math.max(0, this.totalImages - this.slidesVisible);
this.maxTranslate = 0;
this.minTranslate = -(maxIndexWhenNoLoop * (slideSize + gapValue));
return {
slideSize,
gapValue
};
}
updateGallery(index, instant = false) {
const isHorizontal = this.orientation === 'horizontal';
if (this.originalSlides.length === 0) return;
const { slideSize, gapValue } = this.calculateDimensions();
let adjustedIndex;
if (this.infinityLoop && this.slidesVisible === 1 && this.totalImages > 1) {
adjustedIndex = index + 1;
} else if (this.infinityLoop && this.totalImages > this.slidesVisible) {
adjustedIndex = index + this.slidesVisible;
} else {
adjustedIndex = index;
}
const offset = adjustedIndex * (slideSize + gapValue);
const transform = isHorizontal
? `translateX(-${offset}px)`
: `translateY(-${offset}px)`;
requestAnimationFrame(() => {
this.galleryInner.style.transition = instant
? 'none'
: 'transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
this.galleryInner.style.transform = transform;
});
const realIndex = index < 0 ? this.totalImages - 1 : (index >= this.totalImages ? 0 : index);
if (this.thumbs && this.thumbs.length > 0 && this.thumbs[realIndex]) {
this.thumbs[realIndex].checked = true;
}
this.updateArrowsState();
this.updatePagination && this.updatePagination();
}
handleDragStart = (e) => {
this.stopAutoplay();
if (!this.enableDrag || this.isTransitioning) return;
const isTouch = e.type === 'touchstart';
this.startPos = isTouch ? e.touches[0][this.orientation === 'horizontal' ? 'clientX' : 'clientY'] : e[this.orientation === 'horizontal' ? 'clientX' : 'clientY'];
this.endPos = this.startPos;
this.startTime = Date.now();
this.velocity = 0;
this.isDragging = true;
this.isDragged = false;
const transform = window.getComputedStyle(this.galleryInner).transform;
if (transform && transform !== 'none') {
const match = transform.match(/matrix.*\((.+)\)/);
if (match) {
const values = match[1].split(',').map(v => parseFloat(v.trim()));
this.startTranslate = this.orientation === 'horizontal' ? values[4] : values[5];
} else {
this.startTranslate = 0;
}
} else {
this.startTranslate = 0;
}
this.dragStart = this.startPos;
this.dragStartTranslate = this.startTranslate;
requestAnimationFrame(() => {
this.galleryInner.style.transition = 'none';
this.galleryInner.style.cursor = 'grabbing';
});
if (!isTouch) {
e.preventDefault();
}
}
handleDragEnd = () => {
if (!this.isDragging || this.isTransitioning) return;
this.isDragging = false;
if (this.rafId) cancelAnimationFrame(this.rafId);
const diff = this.endPos - this.startPos;
if (Math.abs(diff) <= 5) {
this.isDragged = false;
this.startPos = 0;
this.endPos = 0;
this.velocity = 0;
requestAnimationFrame(() => {
this.galleryInner.style.cursor = 'grab';
});
this.restartAutoplay();
return;
}
const slidePixel = this.slideSize + (Number(this.gap) || 0);
let movedSlides = Math.round(Math.abs(diff) / slidePixel);
if (movedSlides === 0) movedSlides = 1;
const transform = window.getComputedStyle(this.galleryInner).transform;
let currentTranslate = 0;
if (transform && transform !== 'none') {
const match = transform.match(/matrix.*\((.+)\)/);
if (match) {
const values = match[1].split(',').map(v => parseFloat(v.trim()));
currentTranslate = this.orientation === 'horizontal' ? values[4] : values[5];
}
}
requestAnimationFrame(() => {
this.galleryInner.style.cursor = 'grab';
});
if (currentTranslate <= this.innerMinTranslate || currentTranslate >= this.innerMaxTranslate) {
const mod = ((0 % this.totalImages) + this.totalImages) % this.totalImages;
this.currentIndex = mod;
this.updateGallery(this.currentIndex, true);
} else {
const swipeTime = Date.now() - this.startTime;
const minSwipeDistance = 30;
const maxSwipeTime = 300;
const isQuickSwipe = swipeTime < maxSwipeTime && Math.abs(diff) > minSwipeDistance;
const isFastSwipe = Math.abs(this.velocity) > 0.3;
let targetIndex;
if (isQuickSwipe || isFastSwipe || Math.abs(diff) > slidePixel / 2) {
if (diff > 0 || this.velocity > 0) {
targetIndex = this.currentIndex - movedSlides;
} else {
targetIndex = this.currentIndex + movedSlides;
}
} else {
targetIndex = this.currentIndex;
}
this.goToSlide(targetIndex);
}
this.startPos = 0;
this.endPos = 0;
this.velocity = 0;
setTimeout(() => {
this.isDragged = false;
}, 100);
this.restartAutoplay();
};
applyBreakpoints() {
if (!this.breakpoints) return;
const width = window.innerWidth;
const breakpointKeys = Object.keys(this.breakpoints)
.map(Number)
.sort((a, b) => a - b);
let activeBreakpoint = null;
for (const bp of breakpointKeys) {
if (width >= bp) {
activeBreakpoint = bp;
} else {
break;
}
}
if (activeBreakpoint !== null && this.breakpoints[activeBreakpoint]) {
const bpOptions = this.breakpoints[activeBreakpoint];
if (bpOptions.gap !== undefined) {
this.gap = bpOptions.gap;
}
if (bpOptions.slidesVisible !== undefined) {
this.slidesVisible = bpOptions.slidesVisible;
}
if (bpOptions.slidesToScroll !== undefined) {
this.slidesToScroll = bpOptions.slidesToScroll;
}
if (bpOptions.infinityLoop !== undefined) {
this.infinityLoop = bpOptions.infinityLoop;
}
if (bpOptions.autoplay !== undefined) {
this.autoplay = bpOptions.autoplay;
}
}
}
removeClones() {
const clones = this.galleryInner.querySelectorAll('.clone');
clones.forEach(clone => clone.remove());
}
setupInfiniteLoop() {
if (!this.infinityLoop) {
return;
}
if (this.totalImages <= 1) {
return;
}
if (this.originalSlides.length > 0) {
this.removeClones();
if (this.slidesVisible === 1) {
const firstClone = this.originalSlides[0].cloneNode(true);
const lastClone = this.originalSlides[this.totalImages - 1].cloneNode(true);
firstClone.classList.add('clone');
lastClone.classList.add('clone');
const markImagesAsClone = (element) => {
const imgs = element.querySelectorAll('img');
imgs.forEach(img => img.classList.add('clone'));
};
markImagesAsClone(firstClone);
markImagesAsClone(lastClone);
this.galleryInner.insertBefore(lastClone, this.galleryInner.firstChild);
this.galleryInner.appendChild(firstClone);
return;
}
const clonesToAdd = this.slidesVisible;
for (let i = 0; i < clonesToAdd; i++) {
const clone = this.originalSlides[i].cloneNode(true);
clone.classList.add('clone');
const imgs = clone.querySelectorAll('img');
imgs.forEach(img => img.classList.add('clone'));
this.galleryInner.appendChild(clone);
}
for (let i = this.totalImages - 1; i >= this.totalImages - clonesToAdd; i--) {
const clone = this.originalSlides[i].cloneNode(true);
clone.classList.add('clone');
const imgs = clone.querySelectorAll('img');
imgs.forEach(img => img.classList.add('clone'));
this.galleryInner.insertBefore(clone, this.galleryInner.firstChild);
}
}
}
updateArrowsState() {
if (!this.infinityLoop && (this.prevBtn || this.nextBtn)) {
const maxIndex = this.totalImages - this.slidesVisible;
if (this.prevBtn) {
if (this.currentIndex <= 0) {
this.prevBtn.style.opacity = '0.5';
this.prevBtn.style.pointerEvents = 'none';
} else {
this.prevBtn.style.opacity = '';
this.prevBtn.style.pointerEvents = '';
}
}
if (this.nextBtn) {
if (this.currentIndex >= maxIndex) {
this.nextBtn.style.opacity = '0.5';
this.nextBtn.style.pointerEvents = 'none';
} else {
this.nextBtn.style.opacity = '';
this.nextBtn.style.pointerEvents = '';
}
}
}
}
handleTransitionEnd = () => {
if (!this.isTransitioning) return;
this.isTransitioning = false;
if (this.infinityLoop) {
const hasClones = (this.slidesVisible === 1 && this.totalImages > 1) ||
(this.totalImages > this.slidesVisible);
if (hasClones) {
if (this.currentIndex >= this.totalImages || this.currentIndex < 0) {
const mod = ((this.currentIndex % this.totalImages) + this.totalImages) % this.totalImages;
this.currentIndex = mod;
this.updateGallery(this.currentIndex, true);
}
}
}
}
goToSlide(index) {
this.stopAutoplay();
if (this.isTransitioning) return;
this.isTransitioning = true;
this.currentIndex = index;
this.updateGallery(this.currentIndex);
this.updatePagination && this.updatePagination();
this.restartAutoplay();
}
nextSlide = () => {
this.stopAutoplay();
if (this.isTransitioning) return;
if (!this.infinityLoop) {
const maxIndex = this.totalImages - this.slidesVisible;
if (this.currentIndex >= maxIndex) {
return;
}
}
this.isTransitioning = true;
this.currentIndex += this.slidesToScroll;
this.updateGallery(this.currentIndex);
this.updatePagination();
if (window.getSelection) {
window.getSelection().removeAllRanges();
}
this.restartAutoplay();
}
prevSlide = () => {
this.stopAutoplay();
if (this.isTransitioning) return;
if (!this.infinityLoop && this.currentIndex <= 0) {
return;
}
this.isTransitioning = true;
this.currentIndex -= this.slidesToScroll;
this.updateGallery(this.currentIndex);
this.updatePagination();
if (window.getSelection) {
window.getSelection().removeAllRanges();
}
this.restartAutoplay();
}
handleDragMove = (e) => {
if (!this.isDragging || this.isTransitioning) return;
const isTouch = e.type === 'touchmove';
const currentPos = isTouch ? e.touches[0][this.orientation === 'horizontal' ? 'clientX' : 'clientY'] : e[this.orientation === 'horizontal' ? 'clientX' : 'clientY'];
const previousPos = this.endPos;
this.endPos = currentPos;
const diff = this.endPos - this.startPos;
if (Math.abs(diff) > 5) this.isDragged = true;
const timeDiff = Date.now() - this.startTime;
if (timeDiff > 0) {
this.velocity = (this.endPos - previousPos) / timeDiff;
}
if (this.rafId) cancelAnimationFrame(this.rafId);
this.rafId = requestAnimationFrame(() => {
const transformProp = this.orientation === 'horizontal' ? 'translateX' : 'translateY';
let nextTranslate = this.startTranslate + diff;
if (typeof this.innerMinTranslate !== 'number' || typeof this.innerMaxTranslate !== 'number') {
const maxDrag = this.slideSize * this.slidesVisible;
if (nextTranslate - this.startTranslate > maxDrag) {
nextTranslate = this.startTranslate + maxDrag;
}
if (nextTranslate - this.startTranslate < -maxDrag) {
nextTranslate = this.startTranslate - maxDrag;
}
} else {
if (nextTranslate > this.innerMaxTranslate) {
nextTranslate = this.innerMaxTranslate;
}
if (nextTranslate < this.innerMinTranslate) {
nextTranslate = this.innerMinTranslate;
}
}
this.galleryInner.style.transform = `${transformProp}(${nextTranslate}px)`;
});
if (!isTouch) {
e.preventDefault();
}
}
handleClickPrevent = (e) => {
if (this.isDragged) {
e.preventDefault();
e.stopPropagation();
}
}
startAutoplay() {
if (!this.autoplay || typeof this.autoplay !== 'number') return;
this.stopAutoplay();
this.autoplayTimer = setInterval(() => {
if (!this.isDragging && !this.isTransitioning) {
this.nextSlide();
}
}, this.autoplay);
}
stopAutoplay() {
if (this.autoplayTimer) {
clearInterval(this.autoplayTimer);
this.autoplayTimer = null;
}
}
restartAutoplay() {
if (!this.autoplay || typeof this.autoplay !== 'number') return;
this.stopAutoplay();
this.startAutoplay();
}
createPagination() {
if (!this.pagination) return;
this.pagination.innerHTML = "";
if (this.infinityLoop) {
this.totalPages = this.totalImages;
} else {
this.totalPages = Math.max(1, this.totalImages - this.slidesVisible + 1);
}
this.bullets = [];
for (let i = 0; i < this.totalPages; i++) {
const bullet = document.createElement("button");
bullet.classList.add("increazy-carousel-bullet");
bullet.dataset.index = i;
bullet.setAttribute("aria-label", `Ir para slide ${i + 1} de ${this.totalPages}`);
bullet.setAttribute("type", "button");
bullet.addEventListener("click", () => {
this.goToPage(i);
});
this.pagination.appendChild(bullet);
this.bullets.push(bullet);
}
this.updatePagination();
}
updatePagination() {
if (!this.bullets || this.bullets.length === 0) return;
const maxIndexWhenNoLoop = Math.max(0, this.totalImages - this.slidesVisible);
let realSlideIndex = this.currentIndex;
if (this.infinityLoop) {
if (this.currentIndex < 0) {
realSlideIndex = this.totalImages - 1;
} else if (this.currentIndex >= this.totalImages) {
realSlideIndex = 0;
}
} else {
realSlideIndex = Math.min(Math.max(0, this.currentIndex), maxIndexWhenNoLoop);
}
let pageIndex;
if (this.infinityLoop) {
pageIndex = realSlideIndex;
} else {
pageIndex = Math.min(realSlideIndex, maxIndexWhenNoLoop);
}
this.bullets.forEach((b, i) => {
b.classList.toggle('increazy-carousel-bullet-active', i === pageIndex);
if (i === pageIndex) {
b.setAttribute('aria-current', 'true');
} else {
b.removeAttribute('aria-current');
}
});
}
goToPage(pageIndex) {
this.stopAutoplay();
if (this.isTransitioning) return;
const maxIndexWhenNoLoop = Math.max(0, this.totalImages - this.slidesVisible);
let targetIndex;
if (this.infinityLoop) {
targetIndex = pageIndex;
} else {
targetIndex = Math.min(Math.max(0, pageIndex), maxIndexWhenNoLoop);
}
this.goToSlide(targetIndex);
}
setupEventListeners() {
if (this.thumbs && this.thumbs.length > 0) {
this.thumbs.forEach((thumb, i) => {
thumb.addEventListener('change', () => {
this.goToSlide(i);
});
});
}
if (this.prevBtn) {
this.prevBtn.addEventListener('click', this.prevSlide);
}
if (this.nextBtn) {
this.nextBtn.addEventListener('click', this.nextSlide);
}
this.galleryInner.addEventListener('transitionend', this.handleTransitionEnd);
if (this.enableDrag) {
this.gallery.addEventListener('touchstart', this.handleDragStart, { passive: true });
this.gallery.addEventListener('touchmove', this.handleDragMove, { passive: true });
this.gallery.addEventListener('touchend', this.handleDragEnd);
this.gallery.addEventListener('touchcancel', this.handleDragEnd);
this.gallery.addEventListener('mousedown', this.handleDragStart);
this.gallery.addEventListener('mousemove', this.handleDragMove);
this.gallery.addEventListener('mouseup', this.handleDragEnd);
this.gallery.addEventListener('mouseleave', this.handleDragEnd);
this.gallery.addEventListener('click', this.handleClickPrevent, true);
this.galleryInner.style.cursor = 'grab';
}
}
setupResizeObserver() {
let resizeTimer;
this.handleResize = () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
this.applyBreakpoints();
this.refresh();
}, 250);
};
window.addEventListener('resize', this.handleResize);
}
init() {
this.isTransitioning = false;
this.currentIndex = 0;
this.removeClones();
if (this.totalImages <= 1) {
this.infinityLoop = false;
}
if (this.totalImages > 1 && this.infinityLoop) {
this.setupInfiniteLoop();
}
if (this.thumbs && this.thumbs.length > 0) {
this.thumbs[0].checked = true;
}
if (this.pagination) {
this.createPagination();
}
this.calculateDimensions();
setTimeout(() => {
this.updateGallery(0, true);
this.startAutoplay();
}, 10);
}
destroy() {
if (this.thumbs && this.thumbs.length > 0) {
this.thumbs.forEach((thumb, i) => {
thumb.removeEventListener('change', () => this.goToSlide(i));
});
}
if (this.prevBtn) {
this.prevBtn.removeEventListener('click', this.prevSlide);
}
if (this.nextBtn) {
this.nextBtn.removeEventListener('click', this.nextSlide);
}
this.galleryInner.removeEventListener('transitionend', this.handleTransitionEnd);
if (this.enableDrag) {
this.gallery.removeEventListener('touchstart', this.handleDragStart);
this.gallery.removeEventListener('touchmove', this.handleDragMove);
this.gallery.removeEventListener('touchend', this.handleDragEnd);
this.gallery.removeEventListener('touchcancel', this.handleDragEnd);
this.gallery.removeEventListener('mousedown', this.handleDragStart);
this.gallery.removeEventListener('mousemove', this.handleDragMove);
this.gallery.removeEventListener('mouseup', this.handleDragEnd);
this.gallery.removeEventListener('mouseleave', this.handleDragEnd);
this.gallery.removeEventListener('click', this.handleClickPrevent, true);
}
if (this.handleResize) {
window.removeEventListener('resize', this.handleResize);
}
this.stopAutoplay();
this.removeClones();
}
refresh() {
this.stopAutoplay();
this.init();
}
}
Estrutura HTML básica
- Estrutura Mínima
- Estrutura Completa
<div class="carousel">
<div class="carousel__inner">
<div class="carousel__slide">Slide 1</div>
<div class="carousel__slide">Slide 2</div>
<div class="carousel__slide">Slide 3</div>
</div>
</div>
<div class="carousel-container">
<!-- Galeria -->
<div class="carousel">
<div class="carousel__inner">
<div class="carousel__slide">Slide 1</div>
<div class="carousel__slide">Slide 2</div>
<div class="carousel__slide">Slide 3</div>
</div>
</div>
<!-- Setas de navegação -->
<button class="carousel__arrow carousel__arrow--prev">←</button>
<button class="carousel__arrow carousel__arrow--next">→</button>
<!-- Paginação -->
<div class="carousel__pagination"></div>
</div>
Inicialização
- Inicialização Básica
- Inicialização Completa
_dom('UniversalIncreazyCarousel').waitVariable(function () {
const carousel = new UniversalIncreazyCarousel({
gallery: document.querySelector('.carousel'),
galleryInner: document.querySelector('.carousel__inner')
});
});
_dom('UniversalIncreazyCarousel').waitVariable(function () {
const carousel = new UniversalIncreazyCarousel({
gallery: document.querySelector('.carousel'),
galleryInner: document.querySelector('.carousel__inner'),
prevBtn: document.querySelector('.carousel__arrow--prev'),
nextBtn: document.querySelector('.carousel__arrow--next'),
pagination: document.querySelector('.carousel__pagination'),
gap: 20,
slidesVisible: 1,
slidesToScroll: 1,
infinityLoop: true,
autoplay: 5000,
enableDrag: true,
orientation: 'horizontal'
});
// Salvar referência para destruição posterior
window.activeCarousels = window.activeCarousels || [];
window.activeCarousels.push(carousel);
});
Opções de Configuração
| Opção | Tipo | Padrão | Descrição |
|---|---|---|---|
gallery | Element | obrigatório | Container principal do carrossel |
galleryInner | Element | obrigatório | Container dos slides |
prevBtn | Element | null | Botão de navegação anterior |
nextBtn | Element | null | Botão de navegação próximo |
pagination | Element | null | Container da paginação |
thumbs | Array | [] | Array de inputs radio para thumbnails |
gap | Number | 0 | Espaçamento entre slides (px) |
slidesVisible | Number | 1 | Quantidade de slides visíveis |
slidesToScroll | Number | 1 | Quantidade de slides a avançar |
infinityLoop | Boolean | true | Ativa loop infinito |
autoplay | Number/Boolean | false | Tempo em ms para autoplay |
enableDrag | Boolean | true | Ativa drag/swipe |
orientation | String | 'horizontal' | Orientação: 'horizontal' ou 'vertical' |
breakpoints | Object | null | Configurações responsivas |
Breakpoints Responsivos
Os breakpoints permitem ajustar o comportamento do carrossel em diferentes tamanhos de tela.
- Estrutura
- Exemplo Prático
breakpoints: {
[larguraMinima]: {
slidesVisible: Number,
slidesToScroll: Number,
gap: Number,
infinityLoop: Boolean,
autoplay: Number/Boolean
}
}
breakpoints: {
0: {
slidesVisible: 1,
gap: 0,
infinityLoop: true
},
561: {
slidesVisible: 2,
gap: 15
},
769: {
slidesVisible: 3,
gap: 15
},
1025: {
slidesVisible: 4,
gap: 30,
infinityLoop: true
}
}
Estilização CSS
- CSS Base
/* Container principal */
.carousel {
width: 100%;
overflow: hidden;
user-select: none;
}
/* Container dos slides */
.carousel__inner {
display: flex;
gap: 30px; /* Deve corresponder ao gap do JS */
transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
will-change: transform;
}
/* Slides individuais */
.carousel__slide {
flex-shrink: 0;
box-sizing: border-box;
/* A largura é calculada dinamicamente pelo JS */
}
O cálculo da largura garante que todos os slides caibam exatamente dentro do espaço disponível, mesmo quando há múltiplos itens visíveis ao mesmo tempo e um gap entre eles.
Sem esse cálculo, os slides podem estourar o layout, quebrar o alinhamento ou criar scroll horizontal.
A fórmula distribui a largura total do carrossel entre os slides visíveis, subtraindo antes o espaço ocupado pelos gaps — garantindo um layout responsivo, preciso e consistente em qualquer resolução.
- Exemplo
.carousel__slide {
width: calc((100% - 90px) / 4);
/* Fórmula: (100% - (gap × (slidesVisible - 1))) / slidesVisible */
}
Exemplos de Cálculo
| Slides Visíveis | Gap | Fórmula CSS |
|---|---|---|
| 1 | 0px | width: 100% |
| 2 | 15px | width: calc((100% - 15px) / 2) |
| 3 | 15px | width: calc((100% - 30px) / 3) |
| 4 | 30px | width: calc((100% - 90px) / 4) |
| 6 | 16px | width: calc((100% - 80px) / 6) |
Exemplo de CSS Completo com Navegação
- Exemplo Completo
.carousel-container {
position: relative;
padding: 40px 0;
}
.carousel {
width: 100%;
overflow: hidden;
user-select: none;
}
.carousel__inner {
display: flex;
gap: 30px;
transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
will-change: transform;
}
.carousel__slide {
flex-shrink: 0;
width: calc((100% - 90px) / 4);
box-sizing: border-box;
}
/* Setas de navegação */
.carousel__arrow {
width: 50px;
height: 50px;
border-radius: 4px;
background-color: #fff;
border: 1px solid #e0e0e0;
position: absolute;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
transition: opacity 0.3s ease;
}
.carousel__arrow--prev {
left: -56px;
}
.carousel__arrow--next {
right: -56px;
}
.carousel__arrow:hover {
background-color: #f5f5f5;
}
/* Paginação */
.carousel__pagination {
display: flex;
gap: 8px;
justify-content: center;
margin-top: 24px;
}
.increazy-carousel-bullet {
width: 16px;
height: 16px;
border: 1px solid #e0e0e0;
border-radius: 50%;
background-color: #fff;
cursor: pointer;
transition: all 0.3s ease;
}
.increazy-carousel-bullet-active {
background-color: #007bff;
border-color: #007bff;
}
/* Responsivo */
@media (max-width: 1024px) {
.carousel__inner {
gap: 15px;
}
.carousel__slide {
width: calc((100% - 30px) / 3);
}
.carousel__arrow {
display: none;
}
}
@media (max-width: 768px) {
.carousel__slide {
width: calc((100% - 15px) / 2);
}
}
@media (max-width: 560px) {
.carousel__inner {
gap: 0;
}
.carousel__slide {
width: 100%;
}
}
Métodos
goToSlide(index)
Navega para um slide específico.
carousel.goToSlide(2); // Vai para o terceiro slide (índice 2)
nextSlide()
Avança para o próximo slide.
carousel.nextSlide();
prevSlide()
Volta para o slide anterior.
carousel.prevSlide();
refresh()
Reinicializa o carrossel (útil após mudanças no DOM)
carousel.refresh();
destroy()
Remove todos os event listeners e limpa o carrossel.
carousel.destroy();
startAutoplay() & stopAutoplay()
Controla o autoplay manualmente.
carousel.startAutoplay();
carousel.stopAutoplay();
Exemplos Práticos
Banner Principal Simples
- Preview
- HTML
- CSS
- Javascript
<div class="main-banner">
<div class="main-banner__inner">
<div class="main-banner__slide">
<img src="banner1.jpg" alt="Banner 1">
</div>
<div class="main-banner__slide">
<img src="banner2.jpg" alt="Banner 2">
</div>
</div>
</div>
<button class="main-banner__arrow main-banner__arrow--next">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.7813 8.66688L2.66665 8.66688L2.66665 7.33355L10.7813 7.33355L7.20531 3.75755L8.14798 2.81488L13.3333 8.00021L8.14798 13.1855L7.20531 12.2429L10.7813 8.66688Z" fill="black"/>
</svg>
</button>
<button class="main-banner__arrow main-banner__arrow--prev">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.21869 7.33361H13.3334V8.66694H5.21869L8.79469 12.2429L7.85202 13.1856L2.66669 8.00027L7.85202 2.81494L8.79469 3.75761L5.21869 7.33361Z" fill="black"/>
</svg>
</button>
.main-banner {
width: 100%;
overflow: hidden;
}
.main-banner__inner {
display: flex;
gap: 0;
transition: transform 0.4s ease;
}
.main-banner__slide {
flex-shrink: 0;
width: 100%;
}
.main-banner__slide img {
width: 100%;
height: auto;
display: block;
}
.main-banner__arrow {
background-color: #fff;
width: 44px;
height: 44px;
border-radius: 800px;
border: 1px solid #E6E6E6;
box-shadow: 0px 1px 8px 0px #0000001F;
position: absolute;
top: 50%;
transform: translateY(-50%);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.main-banner__arrow svg {
width: 16px;
height: 16px;
}
.main-banner__arrow--prev {
left: 15px;
}
.main-banner__arrow--next {
right: 15px;
}
_dom('UniversalIncreazyCarousel').waitVariable(function () {
const bannerCarousel = new UniversalIncreazyCarousel({
gallery: document.querySelector('.main-banner'),
galleryInner: document.querySelector('.main-banner__inner'),
prevBtn: document.querySelector('.main-banner__arrow--prev'),
nextBtn: document.querySelector('.main-banner__arrow--next'),
gap: 0,
autoplay: 7000,
infinityLoop: true
});
window.activeCarousels = window.activeCarousels || [];
window.activeCarousels.push(bannerCarousel);
});
Galeria de Produtos
- Preview
- HTML
- CSS
- Javascript
<div class="products-carousel">
<div class="products-carousel__inner">
<div class="products-carousel__slide">Produto 1</div>
<div class="products-carousel__slide">Produto 2</div>
<div class="products-carousel__slide">Produto 3</div>
<div class="products-carousel__slide">Produto 4</div>
<div class="products-carousel__slide">Produto 5</div>
<div class="products-carousel__slide">Produto 6</div>
</div>
</div>
<button class="products-carousel__arrow products-carousel__arrow--prev">←</button>
<button class="products-carousel__arrow products-carousel__arrow--next">→</button>
<div class="products-carousel__pagination"></div>
.products-carousel {
overflow: hidden;
}
.products-carousel__inner {
display: flex;
gap: 20px;
transition: transform 0.4s ease;
}
.products-carousel__slide {
width: calc((100% - 60px) / 4);
flex-shrink: 0;
box-sizing: border-box;
}
.products-carousel__arrow {
background-color: #fff;
width: 44px;
height: 44px;
border-radius: 800px;
border: 1px solid #E6E6E6;
box-shadow: 0px 1px 8px 0px #0000001F;
position: absolute;
top: 50%;
transform: translateY(-50%);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.products-carousel__arrow svg {
width: 16px;
height: 16px;
}
.products-carousel__arrow--prev {
left: 15px;
}
.products-carousel__arrow--next {
right: 15px;
}
.products-carousel__pagination {
width: max-content;
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 8px;
height: 20px;
margin-inline: auto;
}
.products-carousel__pagination .increazy-carousel-bullet {
width: 16px;
min-width: 16px;
height: 16px;
border: 1px solid #e6e6e6;
border-radius: 100%;
transition: all .3s ease;
background-color: #ffffff;
cursor: pointer;
}
.products-carousel__pagination .increazy-carousel-bullet-active {
background-color: #000000;
border-color: #000000;
}
@media (max-width: 1024px) {
.products-carousel__inner {
gap: 15px;
}
.products-carousel__slide {
width: calc((100% - 30px) / 3);
}
}
@media (max-width: 768px) {
.products-carousel__slide {
width: calc((100% - 15px) / 2);
}
}
@media (max-width: 560px) {
.products-carousel__inner {
gap: 0;
}
.products-carousel__slide {
width: 100%;
}
}
_dom('UniversalIncreazyCarousel').waitVariable(function () {
const productsCarousel = new UniversalIncreazyCarousel({
gallery: document.querySelector('.products-carousel'),
galleryInner: document.querySelector('.products-carousel__inner'),
prevBtn: document.querySelector('.products-carousel__arrow--prev'),
nextBtn: document.querySelector('.products-carousel__arrow--next'),
pagination: document.querySelector('.products-carousel__pagination'),
gap: 20,
slidesVisible: 4,
slidesToScroll: 4,
infinityLoop: false,
breakpoints: {
0: { slidesVisible: 1, gap: 0 },
561: { slidesVisible: 2, gap: 15 },
769: { slidesVisible: 3, gap: 15 },
1025: { slidesVisible: 4, gap: 20 }
}
});
window.activeCarousels = window.activeCarousels || [];
window.activeCarousels.push(productsCarousel);
});
Inicializar Múltiplos Carrosséis na Página
- Javascript
_dom('UniversalIncreazyCarousel').waitVariable(function () {
window.activeCarousels = window.activeCarousels || [];
const showcases = document.querySelectorAll('[data-showcase]');
showcases.forEach(showcase => {
const carousel = new UniversalIncreazyCarousel({
gallery: showcase.querySelector('.showcase__gallery'),
galleryInner: showcase.querySelector('.showcase__inner'),
prevBtn: showcase.querySelector('.showcase__arrow--prev'),
nextBtn: showcase.querySelector('.showcase__arrow--next'),
pagination: showcase.querySelector('.showcase__pagination'),
breakpoints: {
0: { slidesVisible: 1, gap: 0 },
561: { slidesVisible: 2, gap: 15 },
769: { slidesVisible: 3, gap: 15 },
1025: { slidesVisible: 4, gap: 30 }
}
});
window.activeCarousels.push(carousel);
});
});
Dicas e Boas Práticas
1. Sincronização CSS-JS
O gap do CSS deve ser idêntico ao gap do JavaScript:
// JavaScript
gap: 30
// CSS
.carousel__inner {
gap: 30px;
}
2. Cálculo de Largura
Use a fórmula correta para cada breakpoint:
width = (100% - (gap × (slidesVisible - 1))) / slidesVisible
3. Gerenciamento de Instâncias
Salve referências dos carrosséis para destruí-los quando necessário:
window.activeCarousels = window.activeCarousels || [];
window.activeCarousels.push(carousel);
// Destruir todos os carrosséis
window.activeCarousels.forEach(c => c.destroy());
window.activeCarousels = [];
Aplicações PWA ou Single Page Applications (SPA) requerem atenção especial! Quando o usuário navega entre páginas sem recarregar o navegador, os carrosséis anteriores permanecem na memória. Isso causa:
❌ Vazamento de memória (memory leaks)
❌ Event listeners duplicados
❌ Comportamento inesperado nos carrosséis
❌ Degradação progressiva de performance
Solução obrigatória: Destrua todos os carrosséis antes de navegar para nova página.
window.activeCarousels = window.activeCarousels || [];
window.onNavigate = (oldHref, newHref, state) => {
const oldOutQuery = oldHref.split('?')[0];
const newOutQuery = newHref.split('?')[0];
// Se está mudando de página (não apenas filtros/query params)
if (oldOutQuery !== newOutQuery) {
if (state === 'start') {
// CRÍTICO: Destruir todos os carrosséis antes de navegar
if (window.activeCarousels && window.activeCarousels.length > 0) {
window.activeCarousels.forEach(carousel => {
if (carousel && typeof carousel.destroy === 'function') {
carousel.destroy();
}
});
window.activeCarousels = [];
}
}
}
}
Na IDE da increazy esse middleware de navegação pode ser encontrado em Elementos > loaders > page.
✅ Sempre destrua antes de navegar
✅ Verifique se o método destroy existe typeof carousel.destroy === 'function'
✅ Limpe o array após destruição window.activeCarousels = []
✅ Faça isso no evento state === 'start' (antes de carregar nova página)
Nota: Se você não destruir os carrosséis em PWA, após 10-15 navegações o usuário pode experimentar travamentos, alto consumo de memória e comportamento errático nos carrosséis.
4. Performance
• Use will-change: transform no container dos slides
• Use loading="lazy" em imagens fora do primeiro slide
5. Acessibilidade
• Adicione aria-label nos botões de navegação
• Use alt descritivos nas imagens
• Garanta contraste adequado nos bullets de paginação