Слайдер (Карусель)
Зачем нужно
Слайдер (carousel) -- компонент для последовательного показа контента: изображений, карточек, отзывов. Два основных подхода: CSS scroll-snap (нативный скролл) и JS-карусель с программным управлением. Оба требуют внимания к доступности, touch-управлению и производительности.
Где используется
- Галерея изображений продукта
- Слайдер с отзывами клиентов
- Карусель карточек товаров
- Hero-баннер на главной странице
- Онбординг (пошаговый тур)
Подход 1: CSS Scroll Snap (нативный)
Самый производительный вариант -- используем нативный скролл браузера с "прилипанием" к слайдам.
HTML
<div class="snap-carousel" role="group" aria-label="Галерея изображений"
aria-roledescription="carousel">
<div class="snap-carousel__track">
<div class="snap-carousel__slide" role="group" aria-roledescription="slide"
aria-label="Слайд 1 из 4">
<img src="img/slide-1.jpg" alt="Описание изображения 1" />
</div>
<div class="snap-carousel__slide" role="group" aria-roledescription="slide"
aria-label="Слайд 2 из 4">
<img src="img/slide-2.jpg" alt="Описание изображения 2" />
</div>
<div class="snap-carousel__slide" role="group" aria-roledescription="slide"
aria-label="Слайд 3 из 4">
<img src="img/slide-3.jpg" alt="Описание изображения 3" />
</div>
<div class="snap-carousel__slide" role="group" aria-roledescription="slide"
aria-label="Слайд 4 из 4">
<img src="img/slide-4.jpg" alt="Описание изображения 4" />
</div>
</div>
</div>
CSS
.snap-carousel__track {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
gap: 16px;
/* Скрыть скроллбар, но сохранить скролл */
scrollbar-width: none;
}
.snap-carousel__track::-webkit-scrollbar {
display: none;
}
.snap-carousel__slide {
flex: 0 0 100%;
scroll-snap-align: start;
}
.snap-carousel__slide img {
width: 100%;
height: 400px;
object-fit: cover;
border-radius: 12px;
}
/* Для нескольких карточек на экране */
@media (min-width: 768px) {
.snap-carousel__slide {
flex: 0 0 calc(33.333% - 11px); /* 3 карточки с gap */
}
}
Подход 2: JS-карусель с кнопками и dots
Полноценная карусель с кнопками "вперёд/назад", точками пагинации и touch-поддержкой.
HTML
<div class="carousel" role="group" aria-label="Карусель отзывов"
aria-roledescription="carousel">
<div class="carousel__viewport">
<div class="carousel__track" aria-live="polite">
<div class="carousel__slide active">
<blockquote>
<p>Отличный сервис, рекомендую!</p>
<cite>-- Алексей</cite>
</blockquote>
</div>
<div class="carousel__slide">
<blockquote>
<p>Быстрая доставка, качественный товар.</p>
<cite>-- Мария</cite>
</blockquote>
</div>
<div class="carousel__slide">
<blockquote>
<p>Лучший магазин в категории.</p>
<cite>-- Дмитрий</cite>
</blockquote>
</div>
</div>
</div>
<!-- Кнопки навигации -->
<button class="carousel__btn carousel__btn--prev" aria-label="Предыдущий слайд">
←
</button>
<button class="carousel__btn carousel__btn--next" aria-label="Следующий слайд">
→
</button>
<!-- Точки пагинации -->
<div class="carousel__dots" role="tablist" aria-label="Навигация по слайдам">
<button class="carousel__dot active" role="tab" aria-selected="true"
aria-label="Слайд 1"></button>
<button class="carousel__dot" role="tab" aria-selected="false"
aria-label="Слайд 2"></button>
<button class="carousel__dot" role="tab" aria-selected="false"
aria-label="Слайд 3"></button>
</div>
</div>
CSS
.carousel {
position: relative;
max-width: 800px;
margin: 0 auto;
}
.carousel__viewport {
overflow: hidden;
border-radius: 12px;
}
.carousel__track {
display: flex;
transition: transform 0.4s ease;
}
.carousel__slide {
flex: 0 0 100%;
padding: 48px 32px;
box-sizing: border-box;
}
/* Кнопки Prev / Next */
.carousel__btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 44px;
height: 44px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
font-size: 18px;
cursor: pointer;
transition: background 0.2s;
z-index: 2;
}
.carousel__btn:hover {
background: #fff;
}
.carousel__btn--prev { left: 12px; }
.carousel__btn--next { right: 12px; }
/* Dots */
.carousel__dots {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 16px;
}
.carousel__dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: none;
background: #ccc;
cursor: pointer;
padding: 0;
transition: background 0.2s, transform 0.2s;
}
.carousel__dot.active {
background: #333;
transform: scale(1.3);
}
JavaScript
class Carousel {
constructor(element) {
this.carousel = element;
this.track = element.querySelector('.carousel__track');
this.slides = [...element.querySelectorAll('.carousel__slide')];
this.dots = [...element.querySelectorAll('.carousel__dot')];
this.prevBtn = element.querySelector('.carousel__btn--prev');
this.nextBtn = element.querySelector('.carousel__btn--next');
this.currentIndex = 0;
this.autoplayTimer = null;
this.touchStartX = 0;
this.touchEndX = 0;
this.init;
}
init {
this.prevBtn.addEventListener('click', () => this.prev);
this.nextBtn.addEventListener('click', () => this.next());
this.dots.forEach((dot, i) => {
dot.addEventListener('click', () => this.goTo(i));
});
// Touch/Swipe
this.track.addEventListener('touchstart', (e) => {
this.touchStartX = e.changedTouches[0].screenX;
this.stopAutoplay;
}, { passive: true });
this.track.addEventListener('touchend', (e) => {
this.touchEndX = e.changedTouches[0].screenX;
this.handleSwipe;
});
// Пауза autoplay при hover
this.carousel.addEventListener('mouseenter', () => this.stopAutoplay);
this.carousel.addEventListener('mouseleave', () => this.startAutoplay);
}
goTo(index) {
this.currentIndex = (index + this.slides.length) % this.slides.length;
this.track.style.transform = `translateX(-${this.currentIndex * 100}%)`;
this.updateDots;
this.updateSlides;
}
prev { this.goTo(this.currentIndex - 1); }
next { this.goTo(this.currentIndex + 1); }
updateDots {
this.dots.forEach((dot, i) => {
dot.classList.toggle('active', i === this.currentIndex);
dot.setAttribute('aria-selected', i === this.currentIndex);
});
}
updateSlides {
this.slides.forEach((slide, i) => {
slide.classList.toggle('active', i === this.currentIndex);
});
}
handleSwipe {
const diff = this.touchStartX - this.touchEndX;
const threshold = 50; // Минимальное расстояние свайпа
if (Math.abs(diff) > threshold) {
diff > 0 ? this.next() : this.prev;
}
this.startAutoplay;
}
startAutoplay(interval = 5000) {
this.stopAutoplay;
this.autoplayTimer = setInterval( => this.next(), interval);
}
stopAutoplay {
if (this.autoplayTimer) {
clearInterval(this.autoplayTimer);
this.autoplayTimer = null;
}
}
}
// Инициализация
const carousel = new Carousel(document.querySelector('.carousel'));
carousel.startAutoplay;
Autoplay с паузой
Autoplay удобен, но должен останавливаться при взаимодействии пользователя.
// Пауза при focus внутри карусели (доступность)
carousel.addEventListener('focusin', () => this.stopAutoplay);
carousel.addEventListener('focusout', () => this.startAutoplay);
// Пауза при prefers-reduced-motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
if (prefersReducedMotion.matches) {
carousel.stopAutoplay;
}
Accessibility-чеклист
| Атрибут | Назначение |
|---|---|
aria-roledescription="carousel" |
Описание компонента |
aria-roledescription="slide" |
Описание слайда |
aria-label="Слайд N из M" |
Текущая позиция |
aria-live="polite" |
Анонсирование смены слайда |
role="tablist" + role="tab" |
Для dots-навигации |
Кнопки с aria-label |
Описание действия |
| Пауза autoplay при focus | Скринридер не теряет позицию |
Частые ошибки
| Ошибка | Проблема | Решение |
|---|---|---|
| Нет touch/swipe | Не работает на мобильных | Добавь touchstart/touchend |
| Autoplay без паузы | Невозможно прочитать контент | Пауза при hover/focus |
| Нет dots-навигации | Нет индикации позиции | Добавь пагинацию |
aria-live отсутствует |
Скринридер не анонсирует смену | Добавь aria-live="polite" |
| Слайдер дёргается | Нет will-change: transform |
Оптимизируй анимацию |
Связанные темы
- Popup модальное окно -- лайтбокс для увеличенных изображений
- Ленивая загрузка изображений -- оптимизация загрузки слайдов
- Скелетон загрузки -- плейсхолдер при загрузке изображений
- Бесконечный скролл -- альтернатива постраничному показу