Как сделать карусель (слайдер) на 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 — никак без хитростей: лента имеет начало и конец. Обычно это и не нужно — честный конец понятнее. Для «бесконечной» ленты берут библиотеку или клонируют крайние слайды.
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.


