Можно реализовать такой слайдер разными способами — чисто CSS (ограниченные возможности), с библиотеками (Swipper, Slick и т.п.) или на чистом JavaScript. Ниже — готовый, минимально полный пример на чистом JS: бесконечный (infinite) карусель через клонирование крайних слайдов, плавный переход, поддержка ресайза, кнопки вперед/назад, индикаторы (dots) и простая поддержка тача (drag).
Скопируйте и вставьте это в HTML-файл (всё в одном файле):
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Slider example</title>
<style>
*{box-sizing:border-box}
body{font-family:Arial;margin:40px;background:#f4f4f4}
.slider{
position:relative;
width:100%;
max-width:800px;
margin:0 auto;
overflow:hidden;
background:#fff;
border-radius:8px;
}
.track{
display:flex;
transition:transform .35s ease;
will-change:transform;
}
.slide{
min-width:100%;
user-select:none;
-webkit-user-drag:none;
padding:30px;
text-align:center;
font-size:24px;
}
/* примеры слайда (можно заменить картинками) */
.slide:nth-child(odd){background:linear-gradient(135deg,#6fb1ff,#8bd3ff)}
.slide:nth-child(even){background:linear-gradient(135deg,#ffd08a,#ffd9b3)}
.controls{
position:absolute;
top:50%;
left:0; right:0;
display:flex;
justify-content:space-between;
transform:translateY(-50%);
pointer-events:none;
}
.btn{
pointer-events:auto;
background:rgba(0,0,0,.5);
color:#fff;
border:none;
margin:0 10px;
width:40px;height:40px;
border-radius:50%;
cursor:pointer;
}
.dots{
text-align:center;
padding:12px 0;
background:transparent;
}
.dot{
display:inline-block;
width:10px;height:10px;
border-radius:50%;
margin:0 6px;
background:rgba(0,0,0,.2);
cursor:pointer;
}
.dot.active{background:rgba(0,0,0,.7)}
</style>
</head>
<body>
<div class="slider" id="slider">
<div class="track" id="track">
<!-- Слайды -->
<div class="slide">Slide 1</div>
<div class="slide">Slide 2</div>
<div class="slide">Slide 3</div>
<div class="slide">Slide 4</div>
</div>
<div class="controls">
<button class="btn" id="prev" aria-label="Prev">❮</button>
<button class="btn" id="next" aria-label="Next">❯</button>
</div>
</div>
<div class="dots" id="dots"></div>
<script>
(function(){
const slider = document.getElementById('slider');
const track = document.getElementById('track');
const prevBtn = document.getElementById('prev');
const nextBtn = document.getElementById('next');
const dotsWrap = document.getElementById('dots');
let slides = Array.from(track.children);
const slideCount = slides.length;
// клонируем первый и последний для бесконечного перехода
const firstClone = slides[0].cloneNode(true);
const lastClone = slides[slides.length - 1].cloneNode(true);
track.appendChild(firstClone);
track.insertBefore(lastClone, track.firstChild);
// обновляем список слайдов (с клонами)
slides = Array.from(track.children);
let index = 1; // начнем со второго узла (первый реальный слайд)
let slideWidth = slider.clientWidth;
let isTransitioning = false;
// выставляем ширину слайдов (каждый 100% контейнера)
function setSizes() {
slideWidth = slider.clientWidth;
slides.forEach(s => s.style.minWidth = slideWidth + 'px');
moveToIndex(index, false);
}
window.addEventListener('resize', debounce(setSizes, 100));
setSizes();
// Dots
const realCount = slideCount;
const dots = [];
for (let i=0;i<realCount;i++){
const d = document.createElement('span');
d.className = 'dot' + (i===0 ? ' active':'');
d.dataset.slide = i;
dotsWrap.appendChild(d);
dots.push(d);
d.addEventListener('click', ()=>goTo(i+1)); // +1 из-за клона в начале
}
function updateDots() {
const realIndex = ((index - 1) % realCount + realCount) % realCount; // 0..realCount-1
dots.forEach((d,i)=>d.classList.toggle('active', i===realIndex));
}
// Перемещение трека
function moveToIndex(i, animate = true){
if (!animate) track.style.transition = 'none';
else track.style.transition = '';
const x = -i * slideWidth;
track.style.transform = `translateX(${x}px)`;
if (!animate){
// принудительно применяем изменения без transition
requestAnimationFrame(()=>{ requestAnimationFrame(()=>{ track.style.transition = ''; }); });
}
}
// переход на индекс
function goTo(i){
if (isTransitioning) return;
isTransitioning = true;
index = i;
moveToIndex(index, true);
updateDots();
}
nextBtn.addEventListener('click', ()=>goTo(index+1));
prevBtn.addEventListener('click', ()=>goTo(index-1));
// Когда завершился переход — проверяем на клоны
track.addEventListener('transitionend', ()=>{
isTransitioning = false;
// если мы перешли на клон (первый элемент — клон последнего)
if (slides[index].isSameNode(firstClone)) {
index = 1; // реальный первый
moveToIndex(index, false);
} else if (slides[index].isSameNode(lastClone)) {
index = realCount; // реальный последний
moveToIndex(index, false);
}
});
// Начальное положение (на реальном слайде)
moveToIndex(index, false);
// Простой drag (mouse/touch) — pointer events
let startX = 0;
let currentTranslate = 0;
let dragging = false;
slider.addEventListener('pointerdown', (e)=>{
slider.setPointerCapture(e.pointerId);
dragging = true;
startX = e.clientX;
currentTranslate = -index * slideWidth;
track.style.transition = 'none';
});
slider.addEventListener('pointermove', (e)=>{
if (!dragging) return;
const dx = e.clientX - startX;
track.style.transform = `translateX(${currentTranslate + dx}px)`;
});
slider.addEventListener('pointerup', (e)=>{
if (!dragging) return;
dragging = false;
const dx = e.clientX - startX;
track.style.transition = '';
// порог для переключения — 20% ширины
if (Math.abs(dx) > slideWidth * 0.2){
if (dx < 0) goTo(index + 1);
else goTo(index - 1);
} else {
moveToIndex(index, true);
}
try { slider.releasePointerCapture(e.pointerId); } catch(e){}
});
slider.addEventListener('pointercancel', ()=>{
if (!dragging) return;
dragging = false;
moveToIndex(index, true);
});
// Автоплей (необязательно)
let autoplayInterval = 4000;
let autoplayId = null;
function startAutoplay(){
stopAutoplay();
autoplayId = setInterval(()=>goTo(index+1), autoplayInterval);
}
function stopAutoplay(){ if (autoplayId) clearInterval(autoplayId); autoplayId = null; }
slider.addEventListener('mouseenter', stopAutoplay);
slider.addEventListener('mouseleave', startAutoplay);
// запустить автоплей, если нужно:
// startAutoplay();
// keyboard
window.addEventListener('keydown', (e)=>{
if (e.key === 'ArrowLeft') prevBtn.click();
if (e.key === 'ArrowRight') nextBtn.click();
});
// утилиты
function debounce(fn, wait){
let t;
return function(...a){ clearTimeout(t); t = setTimeout(()=>fn.apply(this,a), wait); };
}
})();
</script>
Комментарии / пояснения:
- Ключевая идея для бесконечного слайдера — клонировать первый и последний слайды и при достижении клона "прыгать" (без анимации) на соответствующий реальный слайд.
- Позиционирование — transform: translateX(-index * width) через flex-контейнер track.
- При ресайзе нужно пересчитать ширину слайда.
- Для поддержки перетаскивания (touch/mouse) удобно использовать pointer events (работает и на тачах, и на мыши).
- Можно добавить lazy-loading картинок, адаптивные колонки (несколько слайдов на экран) — тогда ширина слайда считается как slider.clientWidth / slidesVisible.
- Если хотите готовое и богатое решение, используйте библиотеку Swiper (маленький код, много функций).
Если хотите — могу:
- прислать версию, где видно картинки вместо цветных блоков,
- сделать вариант с несколькими видимыми слайдами (например 3 на десктопе, 1 на мобиле),
- или показать вариант с использованием CSS-only (radio + labels) или с библиотекой Swiper.