Создание контент-слайдера на HTML, CSS и JavaScript

Создание контент-слайдера на HTML, CSS и JavaScript начинается с окна, дорожки и слайдов. Вы соберете адаптивный компонент без библиотек, добавите точки, стрелки, автопрокрутку и свайп. Процентный сдвиг сохранит работу слайдера при изменении ширины окна.

Как устроен контент-слайдер: окно, дорожка и навигация

Окно показывает один слайд и скрывает соседние элементы. Дорожка выстраивает слайды в строку и перемещается внутри окна.

Схема окна и дорожки контент-слайдера

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

Окно слайдера скрывает соседние слайды

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

Flex-дорожка со слайдами внутри окна

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 и клавиатурное управление.

Сначала проверьте индекс, стрелки и точки. После этого добавляйте автопрокрутку и свайп: так легче найти ошибку в конкретной функции.

Совет от нашей редакции: храните состояние в индексе и меняйте дорожку только через одну функцию. Такой порядок упрощает поддержку стрелок, точек, таймера и свайпа.

Комментарии

Оставьте свой комментарий
VS
Vitaly S.

Спасибо большое.

ДИ
Дима И.

Класс. Спасибо большое очень выручили )
Есть вопрос один, возможно сделать чтобы после последнего не слайдер не отматывался влево, а вправую сторну появлялись первые изображения ?

Б
Бедро

Uncaught TypeError: Cannot read property 'classList' of undefined. Выдает вот такую ошибку

B
BaNru

Ошибка говорит о том, что не может найти 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. В чем может быть проблема не подскажите, где кроется привязка...

B
BaNru

Не видя код - сложно предсказать проблему.
Выложите на http://jsfiddle.net/

АМ
Анна М.
➞ BaNru

http://jsfiddle.net/pfh64zgy/ надеюсь правильно сделала

B
BaNru
➞ BaNru

Данный слайдер не предусматривает изначально создание двух и более слайдеров. По хорошему - надо менять алгоритм и переписывать функции.

Ну на скорую руку можно поизвращаться вот так
http://jsfiddle.net/BaNru/pfh64zgy/1/