Создание контент-слайдера на HTML, CSS и JavaScript
Создание контент-слайдера на HTML, CSS и JavaScript начинается с окна, дорожки и слайдов. Вы соберете адаптивный компонент без библиотек, добавите точки, стрелки, автопрокрутку и свайп. Процентный сдвиг сохранит работу слайдера при изменении ширины окна.
- Как устроен контент-слайдер: окно, дорожка и навигация
- Разметка слайдера на HTML
- Стили: окно, дорожка и слайды
- Переключение слайдов на JavaScript
- Плавный переход
- Автопрокрутка слайдов
- Свайп на телефоне
- Доступность слайдера
- Слайдер на CSS scroll-snap
- Полный код слайдера
- Почему слайдер дергается или ломается: ошибки при сборке
Как устроен контент-слайдер: окно, дорожка и навигация
Окно показывает один слайд и скрывает соседние элементы. Дорожка выстраивает слайды в строку и перемещается внутри окна.

Окно ограничивает видимую область, дорожка хранит слайды, точки выбирают индекс, а стрелки меняют соседние элементы.

Свойство overflow: hidden обрезает дорожку по границам окна. Соседние слайды остаются в DOM, но не выступают наружу.

Flex-контейнер размещает слайды последовательно. Каждый слайд получает ширину окна, поэтому дорожка перемещает элементы на одинаковое расстояние.

Точка хранит индекс слайда в атрибуте data-slide. Кнопка паузы управляет автоматической сменой, а стрелки вызывают переход к соседнему индексу.
Разметка слайдера на HTML
Соберите контейнер, окно, дорожку и элементы управления. Кнопки делайте настоящими <button>, чтобы браузер дал им фокус и клавиатурное управление.
Слайды могут содержать текст, локальные картинки или другой HTML-контент. Храните картинки на своем сайте и не подключайте ресурсы по незащищенному HTTP.
Контейнер подпишите через aria-label. Стрелкам задайте тип button и понятные подписи, чтобы клик внутри формы не отправлял ее.
Стили: окно, дорожка и слайды
Разделите CSS на три задачи: окно обрезает дорожку, flex выстраивает слайды, каждый слайд занимает 100% ширины.
Задайте окну overflow: hidden и aspect-ratio. Браузер вычислит высоту по пропорциям, поэтому фиксированная высота не понадобится.
Правило flex: 0 0 100% оставляет слайду ширину окна. Свойство min-width: 0 не дает длинному тексту растянуть flex-ряд.
Для окна используйте touch-action: pan-y. Браузер сохранит вертикальную прокрутку страницы, а скрипт обработает горизонтальный жест.
Переключение слайдов на JavaScript
Состояние слайдера хранит переменная index. Функция переключения нормализует индекс, меняет transform и обновляет точки.
Выражение translateX(-index * 100%) сдвигает дорожку на долю окна. Процентный сдвиг не требует пересчета пикселей при resize.
Зацикливание возвращает последний индекс к нулю. Переход назад от нулевого индекса приводит к последнему слайду.

Индекс связывает активный слайд с навигацией. CSS получает новое процентное значение, поэтому ширина окна может измениться без хранения координат.
Плавный переход
CSS должен анимировать свойство, которое меняет JavaScript. Для дорожки задайте transition: transform .4s ease.
Документация MDN описывает работу свойств transition и transform.
Медиа-условие prefers-reduced-motion сокращает длительность перехода для пользователя, который отключил лишнее движение. Состояние слайдов и управление сохраняются.
Автопрокрутка слайдов
Храните идентификатор одного интервала в переменной timer. Перед запуском нового интервала вызывайте clearInterval, иначе несколько таймеров начнут менять один индекс.
Ручное переключение останавливает отсчет, меняет слайд и запускает таймер заново. Наведение курсора и фокус внутри контейнера тоже останавливают движение.
Событие visibilitychange останавливает таймер в скрытой вкладке. Кнопка паузы меняет aria-pressed и текст между «Пауза» и «Продолжить».
Автоматическая смена слайдов должна иметь способ остановки или паузы. Это требование описывает критерий WCAG 2.2.2 «Пауза, остановка, скрытие».
Свайп на телефоне
Pointer Events передают координаты мыши, пера и касания через один API. При нажатии сохраните clientX, а при отпускании вычислите разницу.
Порог в 50 пикселей отсекает случайные движения. Маленький жест не меняет индекс и не мешает вертикальной прокрутке страницы.
Проверяйте цель события перед началом жеста. Если пользователь нажал стрелку, обработчик свайпа пропустит кнопку и оставит ей обычный клик.
Доступность слайдера
Контейнер получает описание карусели, а каждый слайд сообщает скринридеру свой номер. Точки называют действие через aria-label, активная точка получает aria-current="true".
Стрелки имеют подписи «Предыдущий слайд» и «Следующий слайд». Окно принимает клавиши ArrowLeft, ArrowRight, Home и End.
Разметка опирается на рекомендации WAI-ARIA Authoring Practices для Carousel. Фокус не переносится принудительно в новый слайд.
Слайдер на CSS scroll-snap
CSS scroll-snap подходит для горизонтальной ленты, которую пользователь листает вручную. Браузер фиксирует прокрутку у края слайда без расчета индекса в JavaScript.
Окну задайте overflow-x: auto и scroll-snap-type: x mandatory. Слайдам добавьте scroll-snap-align: start и ширину для выбранного числа карточек.
Такой вариант подходит для свободного свайпа и карточек. Для автопрокрутки, одной активной точки и полного контроля состояния используйте дорожку и индекс.
Полный код слайдера
Скопируйте листинг в локальный HTML-файл и откройте его в браузере. Внутри слайдов используется текст, поэтому пример не зависит от внешних картинок и библиотек.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Адаптивный контент-слайдер</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; font: 1rem/1.5 system-ui, sans-serif; color: #172033; background: #f3f5f8; }
.slider { max-width: 720px; margin: 2rem auto; padding: 1rem; }
.slider__window { position: relative; overflow: hidden; aspect-ratio: 16 / 9; border-radius: .75rem; background: #18243a; touch-action: pan-y; }
.slider__track { display: flex; height: 100%; transform: translateX(0); transition: transform .4s ease; }
.slider__slide { display: grid; flex: 0 0 100%; min-width: 0; place-items: center; padding: 2rem; color: #fff; text-align: center; }
.slider__slide:nth-child(1) { background: #2856a6; }
.slider__slide:nth-child(2) { background: #8b3d68; }
.slider__slide:nth-child(3) { background: #28745b; }
.slider__slide h2 { margin: 0 0 .5rem; }
.slider__slide p { margin: 0; }
.slider__arrow, .slider__pause, .slider__dot { border: 0; cursor: pointer; font: inherit; }
.slider__arrow { position: absolute; top: 50%; width: 2.5rem; height: 2.5rem; border-radius: 50%; color: #172033; background: #fff; transform: translateY(-50%); }
.slider__arrow--prev { left: .75rem; }
.slider__arrow--next { right: .75rem; }
.slider__controls { display: flex; align-items: center; justify-content: center; gap: .75rem; margin-top: 1rem; }
.slider__dots { display: flex; gap: .5rem; }
.slider__dot { width: .8rem; height: .8rem; padding: 0; border-radius: 50%; background: #aab2c0; }
.slider__dot[aria-current="true"] { background: #2856a6; }
.slider__pause { padding: .4rem .7rem; color: #fff; background: #2856a6; border-radius: .35rem; }
.slider button:focus-visible { outline: 3px solid #f3a712; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .slider__track { transition-duration: .01ms; } }
</style>
</head>
<body>
<section class="slider" aria-roledescription="carousel" aria-label="Пример контент-слайдера">
<div class="slider__window" tabindex="0">
<div class="slider__track">
<article class="slider__slide" role="group" aria-roledescription="slide" aria-label="Слайд 1 из 3"><div><h2>Первый слайд</h2><p>Текстовый контент слайдера.</p></div></article>
<article class="slider__slide" role="group" aria-roledescription="slide" aria-label="Слайд 2 из 3"><div><h2>Второй слайд</h2><p>Карточка сохраняет ширину окна.</p></div></article>
<article class="slider__slide" role="group" aria-roledescription="slide" aria-label="Слайд 3 из 3"><div><h2>Третий слайд</h2><p>Дорожка движется через transform.</p></div></article>
</div>
<button class="slider__arrow slider__arrow--prev" type="button" aria-label="Предыдущий слайд">‹</button>
<button class="slider__arrow slider__arrow--next" type="button" aria-label="Следующий слайд">›</button>
</div>
<div class="slider__controls">
<div class="slider__dots" aria-label="Выбор слайда">
<button class="slider__dot" type="button" data-slide="0" aria-label="Показать слайд 1" aria-current="true"></button>
<button class="slider__dot" type="button" data-slide="1" aria-label="Показать слайд 2" aria-current="false"></button>
<button class="slider__dot" type="button" data-slide="2" aria-label="Показать слайд 3" aria-current="false"></button>
</div>
<button class="slider__pause" type="button" aria-pressed="false">Пауза</button>
</div>
</section>
<script>
const slider = document.querySelector('.slider');
const windowElement = slider.querySelector('.slider__window');
const track = slider.querySelector('.slider__track');
const slides = [...slider.querySelectorAll('.slider__slide')];
const dots = [...slider.querySelectorAll('.slider__dot')];
const previous = slider.querySelector('.slider__arrow--prev');
const next = slider.querySelector('.slider__arrow--next');
const pauseButton = slider.querySelector('.slider__pause');
const delay = 4000;
let index = 0;
let timer = null;
let paused = false;
let pointerOver = false;
let focusWithin = false;
let pointerStart = null;
function render() {
track.style.transform = `translateX(-${index * 100}%)`;
dots.forEach((dot, dotIndex) => dot.setAttribute('aria-current', String(dotIndex === index)));
}
function setSlide(nextIndex) {
index = (nextIndex + slides.length) % slides.length;
render();
restartTimer();
}
function startTimer() {
if (paused || pointerOver || focusWithin || document.visibilityState !== 'visible') return;
stopTimer();
timer = window.setInterval(() => setSlide(index + 1), delay);
}
function stopTimer() {
window.clearInterval(timer);
timer = null;
}
function restartTimer() {
stopTimer();
startTimer();
}
previous.addEventListener('click', () => setSlide(index - 1));
next.addEventListener('click', () => setSlide(index + 1));
dots.forEach((dot) => dot.addEventListener('click', () => setSlide(Number(dot.dataset.slide))));
pauseButton.addEventListener('click', () => {
paused = !paused;
pauseButton.setAttribute('aria-pressed', String(paused));
pauseButton.textContent = paused ? 'Продолжить' : 'Пауза';
paused ? stopTimer() : restartTimer();
});
slider.addEventListener('pointerenter', () => { pointerOver = true; stopTimer(); });
slider.addEventListener('pointerleave', () => { pointerOver = false; restartTimer(); });
slider.addEventListener('focusin', () => { focusWithin = true; stopTimer(); });
slider.addEventListener('focusout', (event) => {
focusWithin = slider.contains(event.relatedTarget);
if (!focusWithin && !pointerOver) restartTimer();
});
document.addEventListener('visibilitychange', () => document.visibilityState === 'visible' ? restartTimer() : stopTimer());
windowElement.addEventListener('keydown', (event) => {
if (event.key === 'ArrowLeft') setSlide(index - 1);
if (event.key === 'ArrowRight') setSlide(index + 1);
if (event.key === 'Home') setSlide(0);
if (event.key === 'End') setSlide(slides.length - 1);
});
windowElement.addEventListener('pointerdown', (event) => {
if (event.target.closest('button')) return;
pointerStart = event.clientX;
});
windowElement.addEventListener('pointerup', (event) => {
if (pointerStart === null) return;
const distance = event.clientX - pointerStart;
if (Math.abs(distance) > 50) setSlide(index + (distance < 0 ? 1 : -1));
pointerStart = null;
});
windowElement.addEventListener('pointercancel', () => { pointerStart = null; });
render();
startTimer();
</script>
</body>
</html>Откройте файл в браузере и нажмите стрелку. Слайд сменится, точка получит активное состояние, а таймер возобновит работу после выхода курсора и фокуса.
Почему слайдер дергается или ломается: ошибки при сборке
- Сдвиг в пикселях. Значение вроде
-550pxподходит только для одной ширины. Храните индекс и используйте проценты. - Неверная цель события. Вложенный элемент может стать
event.target. Для кнопок используйтеevent.currentTargetили индекс из замыкания. - Два интервала. Новый
setIntervalбезclearIntervalускоряет смену. Храните один идентификатор таймера. - Переход по старому свойству. Когда JavaScript меняет
transform, правилоtransition: left .4sничего не анимирует. - Свайп без touch-action. Если окно не получает
touch-action: pan-y, браузер может перехватить горизонтальный жест. - Неполная доступность. Точки без подписей и слайды без номеров непонятны скринридеру. Добавьте
aria-label,aria-currentи клавиатурное управление.
Сначала проверьте индекс, стрелки и точки. После этого добавляйте автопрокрутку и свайп: так легче найти ошибку в конкретной функции.
Совет от нашей редакции: храните состояние в индексе и меняйте дорожку только через одну функцию. Такой порядок упрощает поддержку стрелок, точек, таймера и свайпа.
Популярное за неделю
- Как создать сайт самому в 2026: 4 способа + пошаговая инструкция 34 мин чтения
- Лучшие конструкторы для создания сайтов — плюсы, минусы и сравнения 22 мин чтения
- Как работает js задержка выполнения через setTimeout 4 мин чтения
- 15 таймеров обратного отсчёта на jQuery и CSS3 2 мин чтения
- 17 бесплатных jQuery‑скриптов для загрузки файлов 5 мин чтения
- Примеры AJAX-запросов с jQuery и PHP 2 мин чтения
Комментарии
Спасибо большое.
Класс. Спасибо большое очень выручили )
Есть вопрос один, возможно сделать чтобы после последнего не слайдер не отматывался влево, а вправую сторну появлялись первые изображения ?
Uncaught TypeError: Cannot read property 'classList' of undefined. Выдает вот такую ошибку
Ошибка говорит о том, что не может найти classList.
Может быть вы обращаетесь не к тому элементу (вешаете не на объект) или он не верно выбран. Чтобы дать более точный ответ необходимо указывать номер строки, на которой появилась ошибка и код из этой строки.
Так например если ошибка появилась на строке
links[activeLink].classList.add("active");
то вероятно нет выбранных элементов в массиве links и/или неверно установлен activeLink.
Второе возможно например если происходит попытка выбора 10го элемента, когда в массиве всего шесть.
Первый вариант - на странице нет элементов с классом itemLinks и строка
var links = document.querySelectorAll(".itemLinks");
выбирает пустой массив. Таким образом далее идёт ошибка, озвученная выше под вторым вариантом: даже первый элемент не может быть выбран, потому что в массиве ничего нет.
В таких случаях обычно пишется "защита"
if (links && links[activeLink]){
links[activeLink].classList.add("active");
} else {
alert('нет элементов'); // Эта строка нужна только во время отладки. В продакшене надо её или удалить, или написать обход, или завершать скрипт.
}
Подскажите пожалуйста как сделать эти кнопки внутри самого слайдера?
Спасибо.
Статья просто супер, вы обьясняете и розьясняете все как на курсах, очень было интерестно ее читать и делать сам слайдер. Молодцы, классно.
Теперь я понял как это все работает и уже делаю свой новенький но с вашим кодом (чуток подредактированным правда)
Еще раз спасибо, за статью!
Здравствуйте! Спасибо за статью!
Возникла проблема, которую не могу решить.
Нужно добавить 2 таких слайдера на страницу, при добавлении второго (ессно все имена меняю и в коде и в стилях), первый слайдер перестает работать.
Добавляю код и скрипт один в один, только к именам классов и пр. добавляю 2. В чем может быть проблема не подскажите, где кроется привязка...
Не видя код - сложно предсказать проблему.
Выложите на http://jsfiddle.net/
http://jsfiddle.net/pfh64zgy/ надеюсь правильно сделала
Данный слайдер не предусматривает изначально создание двух и более слайдеров. По хорошему - надо менять алгоритм и переписывать функции.
Ну на скорую руку можно поизвращаться вот так
http://jsfiddle.net/BaNru/pfh64zgy/1/