Практика: карусель изображений

Free Preview
Продолжительность:

Пришло время применить всё, что мы узнали о событиях и манипуляции DOM. Создадим карусель изображений — компонент, который автоматически листает фотографии Санкт-Петербурга и позволяет управлять им вручную.

Что будем делать

Карусель будет:

  • автоматически менять слайды каждые 3 секунды
  • реагировать на кнопки «вперёд» и «назад»
  • показывать точки-индикаторы текущего слайда
  • останавливать автоматическую прокрутку при ручном управлении

HTML-структура

Добавьте в разметку страницы блок карусели — например, после секции hero:

<section class="carousel-section">
  <div class="carousel">
    <div class="carousel__track">
      <div class="carousel__slide active">
        <img src="https://s3.twcstorage.ru/f280a6ef-learning-hub-test/lessons/frontend-basics/assets/spb-hermitage.jpg" alt="Эрмитаж">
        <p class="carousel__caption">Эрмитаж — один из крупнейших музеев мира</p>
      </div>
      <div class="carousel__slide">
        <img src="https://s3.twcstorage.ru/f280a6ef-learning-hub-test/lessons/frontend-basics/assets/spb-peterhof.jpg" alt="Петергоф">
        <p class="carousel__caption">Петергоф — дворцово-парковый ансамбль на берегу Финского залива</p>
      </div>
      <div class="carousel__slide">
        <img src="https://s3.twcstorage.ru/f280a6ef-learning-hub-test/lessons/frontend-basics/assets/spb-nevsky.jpg" alt="Невский проспект">
        <p class="carousel__caption">Невский проспект — главная улица города</p>
      </div>
      <div class="carousel__slide">
        <img src="https://s3.twcstorage.ru/f280a6ef-learning-hub-test/lessons/frontend-basics/assets/spb-isaakiy.jpg" alt="Исаакиевский собор">
        <p class="carousel__caption">Исаакиевский собор — символ имперского Петербурга</p>
      </div>
    </div>

    <button class="carousel__btn carousel__btn--prev" aria-label="Предыдущий слайд">&#8592;</button>
    <button class="carousel__btn carousel__btn--next" aria-label="Следующий слайд">&#8594;</button>

    <div class="carousel__dots"></div>
  </div>
</section>
html

CSS

Идея карусели простая: все слайды лежат в одном контейнере, но видно только тот, у которого есть класс active. Остальные скрыты через opacity и position: absolute — это даёт плавный переход без прыжков:

.carousel-section {
  padding: 60px 20px;
  background: #f8f8f8;
}

.carousel {
  position: relative;
  max-width: 800px;
  margin: 0 auto;
  overflow: hidden;
  border-radius: 12px;
}

.carousel__track {
  position: relative;
  height: 450px;
}

.carousel__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.5s ease;
  pointer-events: none;
}

.carousel__slide.active {
  opacity: 1;
  pointer-events: auto;
}

.carousel__slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.carousel__caption {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 16px 20px;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.6));
  color: #fff;
  margin: 0;
}

.carousel__btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(255, 255, 255, 0.85);
  border: none;
  border-radius: 50%;
  width: 44px;
  height: 44px;
  font-size: 18px;
  cursor: pointer;
  transition: background 0.2s ease;
  z-index: 1;
}

.carousel__btn:hover {
  background: #fff;
}

.carousel__btn--prev { left: 12px; }
.carousel__btn--next { right: 12px; }

.carousel__dots {
  position: absolute;
  bottom: 12px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 8px;
  z-index: 1;
}

.carousel__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.5);
  cursor: pointer;
  transition: background 0.2s ease;
  border: none;
  padding: 0;
}

.carousel__dot.active {
  background: #fff;
}
css

JavaScript

Теперь самое интересное. Создайте отдельный файл carousel.js и подключите его к странице с атрибутом defer:

<script src="carousel.js" defer></script>
html

Логика карусели:

const slides = document.querySelectorAll('.carousel__slide');
const dotsContainer = document.querySelector('.carousel__dots');
const prevBtn = document.querySelector('.carousel__btn--prev');
const nextBtn = document.querySelector('.carousel__btn--next');

let current = 0;
let autoplayTimer = null;

// --- Создаём точки-индикаторы ---
slides.forEach((_, i) => {
  const dot = document.createElement('button');
  dot.classList.add('carousel__dot');
  dot.setAttribute('aria-label', `Слайд ${i + 1}`);
  if (i === 0) dot.classList.add('active');
  dotsContainer.append(dot);
});

const dots = document.querySelectorAll('.carousel__dot');

// --- Переключение слайда ---
function goTo(index) {
  slides[current].classList.remove('active');
  dots[current].classList.remove('active');

  current = (index + slides.length) % slides.length; // зацикливаем

  slides[current].classList.add('active');
  dots[current].classList.add('active');
}

// --- Автовоспроизведение ---
function startAutoplay() {
  autoplayTimer = setInterval(() => {
    goTo(current + 1);
  }, 3000);
}

function stopAutoplay() {
  clearInterval(autoplayTimer);
}

startAutoplay();

// --- Кнопки управления ---
prevBtn.addEventListener('click', () => {
  stopAutoplay();
  goTo(current - 1);
});

nextBtn.addEventListener('click', () => {
  stopAutoplay();
  goTo(current + 1);
});

// --- Клик по точкам через делегирование ---
dotsContainer.addEventListener('click', (e) => {
  const dot = e.target.closest('.carousel__dot');
  if (!dot) return;

  stopAutoplay();
  const index = Array.from(dots).indexOf(dot);
  goTo(index);
});

// --- Навигация с клавиатуры ---
document.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowLeft') {
    stopAutoplay();
    goTo(current - 1);
  }
  if (e.key === 'ArrowRight') {
    stopAutoplay();
    goTo(current + 1);
  }
});
js

Разберём несколько ключевых моментов.

(index + slides.length) % slides.length — это трюк для зацикливания. Когда index равен -1 (ушли левее первого слайда), формула даёт slides.length - 1 — то есть последний слайд. Когда index равен slides.length — получаем 0, первый слайд.

stopAutoplay при ручном управлении — хорошая практика: если пользователь сам нажал кнопку, автоматическая прокрутка останавливается. Это не раздражает и даёт ощущение контроля. Можно доработать: возобновлять автовоспроизведение через несколько секунд после последнего взаимодействия.

Делегирование для точек — вместо того чтобы вешать обработчик на каждую точку, мы поставили один обработчик на контейнер. Как мы разбирали в предыдущем уроке.

Клавиатурная навигация — дополнительный штрих. Стрелки влево/вправо переключают слайды без мыши. Это и удобно, и хорошая практика с точки зрения доступности.

Ожидаемый результат

После выполнения задания на странице должна появиться карусель, которая:

  • показывает слайды с фотографиями Петербурга
  • автоматически переключает их каждые 3 секунды
  • переключается вперёд и назад по кнопкам
  • подсвечивает активную точку-индикатор
  • останавливает автопрокрутку при ручном управлении
  • реагирует на стрелки клавиатуры

Если хочется усложнить задачу — попробуйте добавить возобновление автовоспроизведения через 5 секунд после последнего взаимодействия с кнопками. Для этого понадобится setTimeout после каждого вызова stopAutoplay.