Гайды

Как сделать карусель (слайдер) на CSS — без библиотеки и с нативным свайпом

Иллюстрация: ряд карточек на рейке, средняя защёлкнута в паз

Попроси нейросеть «сделай слайдер с фото» — и в проект часто приедет библиотека на десятки килобайт. Со своим скриптом свайпа, своей инерцией и своими багами на айфоне.

А теперь неожиданное: браузер давно умеет листать карточки сам. Пальцем, тачпадом, колёсиком, клавишами. Нужно только сказать ему «останавливайся ровно на карточке». Это одно CSS-свойство — scroll-snap.

Соберём карусель по шагам. В конце будет свайп, стрелки, точки и ни одной зависимости.

Шаг 1. Разметка

Обычный список в обёртке. Никаких классов-слайдов от библиотеки:

<div class="carousel" role="region" aria-label="Фото товара" tabindex="0">
  <div class="slide"><img src="1.webp" alt="Вид спереди" /></div>
  <div class="slide"><img src="2.webp" alt="Вид сбоку" /></div>
  <div class="slide"><img src="3.webp" alt="В руке" /></div>
</div>

role="region" и aria-label скажут скринридеру, что это за блок. Подробнее — в статье про ARIA. А tabindex="0" пустит туда фокус с клавиатуры: стрелки ← → начнут листать.

Шаг 2. Ряд с прокруткой вбок

.carousel {
  display: flex;
  gap: 12px;
  overflow-x: auto;
}
.slide {
  flex: 0 0 85%;
}
.slide img {
  width: 100%;
  display: block;
}

flex: 0 0 85% — каждая карточка занимает 85% ширины и не сжимается. Остальные 15% — краешек следующей. Это подсказка пальцу: «тут есть ещё, листай».

Сейчас ряд уже листается. Но останавливается где попало — на половине карточки.

Шаг 3. Защёлка — scroll-snap

Добавь три строки:

.carousel {
  scroll-snap-type: x mandatory;
}
.slide {
  scroll-snap-align: center;
  scroll-snap-stop: always;
}

Что они делают:

  • scroll-snap-type: x mandatory — по горизонтали после прокрутки обязательно доехать до точки защёлки.
  • scroll-snap-align: center — точка защёлки — центр карточки. Можно start, тогда карточка прилипнет к левому краю.
  • scroll-snap-stop: always — сильный свайп не пролетит три карточки разом. Одна за раз.

Всё, на телефоне это уже полноценная карусель. Инерция, отскок, плавность — родные, системные. Их не нужно эмулировать на JS.

Шаг 4. Спрячь полосу прокрутки и почини «Назад»

.carousel {
  scrollbar-width: none;
  overscroll-behavior-x: contain;
}
.carousel::-webkit-scrollbar {
  display: none;
}

Первая и третья строка прячут полосу прокрутки — в разных браузерах по-разному, поэтому обе.

Вторая важнее, чем кажется. Без неё, долистав до последней карточки и потянув дальше, человек в Chrome может уйти назад по истории. Карусель «передаёт» лишнюю прокрутку странице. contain эту передачу обрезает.

Если страница при этом начала ездить вбок целиком — проверь, не вылезает ли что-то за экран. Разбор в статье «Почему страница скроллится вбок на телефоне».

Шаг 5. Стрелки — 5 строк JS

На компьютере мышкой вбок листать неудобно. Добавь кнопки:

<button class="prev" aria-label="Назад">←</button>
<button class="next" aria-label="Вперёд">→</button>
const track = document.querySelector(".carousel");
const step = () => track.clientWidth * 0.85;
document.querySelector(".next").onclick = () =>
  track.scrollBy({ left: step(), behavior: "smooth" });
document.querySelector(".prev").onclick = () =>
  track.scrollBy({ left: -step(), behavior: "smooth" });

Здесь и есть хитрость. Тебе не нужно считать, где какая карточка. Сдвинь примерно на ширину — защёлка из шага 3 сама доведёт до ровного места.

Шаг 6. Точки, которые знают текущий слайд

Точки — это элементы .dot под каруселью, по одному на слайд. Какая карточка сейчас по центру, подскажет IntersectionObserver. Он сам следит, что видно в контейнере:

const dots = document.querySelectorAll(".dot");
const io = new IntersectionObserver((entries) => {
  for (const e of entries) {
    if (e.isIntersecting) {
      const i = [...track.children].indexOf(e.target);
      dots.forEach((d, j) => d.classList.toggle("active", i === j));
    }
  }
}, { root: track, threshold: 0.6 });
track.querySelectorAll(".slide").forEach((s) => io.observe(s));

threshold: 0.6 — карточка считается текущей, когда видно 60% её площади. Не нужно слушать scroll и пересчитывать на каждый пиксель.

Шаг 7. Уважай тех, кого укачивает

Плавная прокрутка у части людей вызывает тошноту. Отключи её, если в системе включено «уменьшить движение»:

const smooth = matchMedia("(prefers-reduced-motion: reduce)").matches
  ? "auto" : "smooth";

И подставь smooth вместо "smooth" в шаге 5. Зачем это и как работает — в статье про prefers-reduced-motion.

Что получится

Карусель на ~15 строк CSS и ~20 строк JS:

  • свайп на телефоне с родной инерцией;
  • одна карточка за свайп, никаких «пролётов»;
  • стрелки и клавиатура на компьютере;
  • точки показывают текущий слайд;
  • никакого ухода «назад» при лишнем свайпе;
  • ноль зависимостей.

Хочешь, чтобы это собрала нейросеть — так и пиши: «карусель на scroll-snap без библиотек, scroll-snap-stop: always, overscroll-behavior-x: contain, точки через IntersectionObserver». Конкретные слова отсекают тяжёлый слайдер по умолчанию.

Нужна ли автопрокрутка?

Чаще нет. Карусель, которая едет сама, мешает читать и раздражает. Если очень нужна — останавливай её при наведении, фокусе и касании. И дай кнопку паузы.

Как сделать бесконечную карусель?

На чистом scroll-snap — никак без хитростей: лента имеет начало и конец. Обычно это и не нужно — честный конец понятнее. Для «бесконечной» ленты берут библиотеку или клонируют крайние слайды.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →