Snap переключение слайдов в слайдере при drag: CSS/JS
Реализуйте резкое (snap) переключение слайдов в слайдере при перетаскивании мышью или касанием. Подходы: CSS scroll-snap, JS с pointer-событиями, порог срабатывания, отключение transition. Примеры кода HTML/CSS/JS.
Как реализовать резкое (snap) переключение слайдов в слайдере при перетаскивании мышью или касании вместо плавного следования карусели за курсором (translate)? Какие подходы использовать: какие события (pointer/touch/mouse), как вычислить порог срабатывания и как отключить плавную анимацию при перетаскивании? Примеры кода (CSS/JS) приветствуются.
Чтобы получить резкое (snap) переключение слайдов при перетаскивании мышью или касании, используйте либо нативный CSS scroll-snap, либо ручную реализацию на JavaScript с pointer‑событиями и управлением transform. В JS‑варианте отключайте CSS‑переходы во время перетаскивания (например, класс .is-dragging → transition: none), вычисляйте порог как долю ширины слайда (обычно 20–30%) и/или учитывайте скорость (velocity), а при отпускании включайте transition для финального snap и корректируйте индекс слайда.
Содержание
- Краткий обзор подходов
- CSS scroll-snap: нативный snap при перетаскивании
- JS: ручной drag с translate и резким snap (рекомендуемый)
- События: pointer vs touch vs mouse
- Порог срабатывания и учёт скорости (velocity)
- Практические советы: отключение анимации, touch-action, производительность и доступность
- Полный пример кода (HTML / CSS / JS)
- Источники
- Заключение
Краткий обзор подходов
Есть два основных пути:
- Нативный CSS scroll-snap: быстро, минимум кода, браузер сам делает «прилипание» к слайдам после прокрутки (подходит для простых галерей). См. практическое описание на Doka Guide.
- Ручной JS‑контроль через translate + pointer‑события: даёт полный контроль над порогом, скоростью, моментом срабатывания и анимацией. Для гибкой логики drag/snap полезны идеи из статей на Habr и пошаговые примеры на WebForMySelf.
Выбор зависит от требований: нужна тонкая логика — берём JS; достаточно стандартного snap — CSS‑scroll-snap экономит время.
CSS scroll-snap: нативный snap при перетаскивании
Если вам подходит поведение «потянул — браузер прилипает к ближайшему слайду», можно сделать так (минимум JS или без него):
<!-- HTML -->
<div class="carousel">
<div class="slide">Слайд 1</div>
<div class="slide">Слайд 2</div>
<div class="slide">Слайд 3</div>
</div>
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch; /* плавность на iOS */
touch-action: pan-y; /* позволяет вертикальную прокрутку страницы */
scroll-behavior: smooth; /* если вы хотите плавный snap при программном скролле */
}
.slide {
flex: 0 0 100%; /* каждый слайд — ширина окна carousel */
scroll-snap-align: start; /* или center */
user-select: none;
-webkit-user-drag: none;
}
Плюсы: простота, аппаратная оптимизация. Минусы: вы не управляете порогом срабатывания и инерцией детально; для сложных UX нужен JS. Подробности — Doka Guide.
JS: ручной drag с translate и резким snap (рекомендуемый)
Идея: во время перетаскивания обновляем transform без перехода (transition: none) — чтобы элемент следовал точно (без «плавного подтягивания»), а при отпускании вычисляем, переключаться ли на соседний слайд и включаем transition для финального snap.
Ключевые шаги:
- Слушаем
pointerdown→ сохраняем startX и время; добавляем класс.is-dragging. - На
pointermoveвычисляемdeltaXи делаемtransform: translateX(...)через requestAnimationFrame. - На
pointerup(иpointercancel) считаем пройденное расстояние и скорость, решаем по порогу/velocity новый индекс, включаем transition и ставимtranslateX(-index * width). - Управление transition:
.is-dragging .track { transition: none; }— выключили плавную анимацию при движении; даём её при snap.
Примерная логика взята и адаптирована из материалов на Habr и WebForMySelf.
События: pointer vs touch vs mouse
Лучше всего использовать Pointer Events (pointerdown, pointermove, pointerup, pointercancel):
- Они унифицируют мышь, касание и стилус.
- Позволяют
setPointerCapture/releasePointerCapture— не потеряете указатель при выходе за пределы элемента. - Поддерживаются в современных браузерах; при необходимости — fallback на
touch/mouse.
Если Pointer Events не подходят, fallback:
- touch:
touchstart/touchmove/touchend(но нужно учитывать passive listeners). - mouse:
mousedown/mousemove/mouseup(пока pointer не поддерживается).
Важно: используйте CSS touch-action (например, touch-action: pan-y или none) вместо event.preventDefault() в touchmove — это безопаснее и не конфликтует с пассивными слушателями. Подробнее — см. примеры выше.
Порог срабатывания и учёт скорости (velocity)
Порог — это как далеко пользователь перетащил слайд относительно его ширины. Простая формула:
- thresholdDistance = slideWidth * 0.25; // 20–30% обычно удобно
- velocity = deltaX / deltaTime; // px / ms
Решение переключиться:
- если |deltaX| > thresholdDistance → переключаем;
- иначе, если |velocity| > velocityThreshold → переключаем в сторону движения (пользователь «смахнул» быстро);
- иначе → остаёмся на текущем слайде.
Рекомендации по значениям:
- threshold: 0.2–0.3 ширины (т.е. 20–30%)
- velocityThreshold: ~0.25–0.4 px/ms (250–400 px/s) — при тестировании выберите чувствительность под вашу верстку
Пример псевдокода:
const threshold = slideWidth * 0.25;
const velocity = deltaX / (now - startTime); // px/ms
if (Math.abs(deltaX) > threshold || Math.abs(velocity) > 0.3) {
index = deltaX < 0 ? index + 1 : index - 1;
}
Учтите: для маленьких экранов velocity может срабатывать чаще — тестируйте.
Практические советы: отключение анимации, touch-action, производительность и доступность
- Отключение анимации при drag: добавляйте класс
.is-draggingи в нёмtransition: noneдля трека. Это убирает «плавное подтягивание» и делает отклик мгновенным. - touch-action: используйте
touch-action: pan-yна контейнере, если хотите чтобы страница продолжала вертикально скроллиться, илиtouch-action: none, если хотите полностью перехватить жесты. - RequestAnimationFrame: обновляйте
transformв RAF изpointermove, чтобы не перегружать основной поток. - will-change:
will-change: transformна треке помогает оптимизации. - Отключите выделение текста и перетаскивание изображений:
user-select: none; -webkit-user-drag: none;. - Обработка
pointercancel— обязательно, чтобы корректно сбрасывать состояние. - Доступность: поддержите keyboard (стрелки влево/вправо),
aria-roledescription="carousel", ролиregionи фокусируемые элементы внутри слайдов. - Тестируйте на реальных устройствах: чувствительность и скорость — вещь субъективная.
Полный пример кода (HTML / CSS / JS)
Ниже — минимальная, но полная реализация с pointer events, порогом и отключением transition во время перетаскивания.
/* HTML */
<div class="slider" id="slider">
<div class="track">
<div class="slide">1</div>
<div class="slide">2</div>
<div class="slide">3</div>
</div>
</div>
/* CSS */
.slider {
overflow: hidden;
position: relative;
touch-action: pan-y; /* разрешаем вертикальную прокрутку страницы */
user-select: none;
}
.track {
display: flex;
transition: transform 300ms cubic-bezier(.22,.9,.37,1);
will-change: transform;
}
.slider.is-dragging .track {
transition: none; /* выключаем плавную анимацию во время drag */
}
.slide {
flex: 0 0 100%;
min-width: 100%;
box-sizing: border-box;
padding: 20px;
text-align: center;
font-size: 48px;
background: #f4f4f4;
border-right: 1px solid #ddd;
}
/* JS */
const slider = document.getElementById('slider');
const track = slider.querySelector('.track');
const slides = Array.from(track.children);
let currentIndex = 0;
let startX = 0;
let currentX = 0;
let deltaX = 0;
let startTime = 0;
let isDragging = false;
let slideWidth = slider.clientWidth;
let rafId = null;
function updateWidth() {
slideWidth = slider.clientWidth;
// выставляем корректный translate после ресайза
track.style.transform = `translateX(${-currentIndex * slideWidth}px)`;
}
window.addEventListener('resize', updateWidth);
updateWidth();
function setTranslate(translate) {
track.style.transform = `translateX(${translate}px)`;
}
slider.addEventListener('pointerdown', (e) => {
if (e.pointerType === 'mouse' && e.button !== 0) return; // только левая кнопка
isDragging = true;
startX = e.clientX;
currentX = startX;
deltaX = 0;
startTime = performance.now();
slider.classList.add('is-dragging');
slider.setPointerCapture(e.pointerId);
});
slider.addEventListener('pointermove', (e) => {
if (!isDragging) return;
currentX = e.clientX;
deltaX = currentX - startX;
if (!rafId) {
rafId = requestAnimationFrame(() => {
const translate = -currentIndex * slideWidth + deltaX;
setTranslate(translate);
rafId = null;
});
}
});
function endDrag(e) {
if (!isDragging) return;
isDragging = false;
slider.classList.remove('is-dragging');
const elapsed = Math.max(1, performance.now() - startTime); // ms
const velocity = deltaX / elapsed; // px / ms
const threshold = slideWidth * 0.25; // 25%
let newIndex = currentIndex;
if (Math.abs(deltaX) > threshold || Math.abs(velocity) > 0.3) {
newIndex = deltaX < 0 ? currentIndex + 1 : currentIndex - 1;
}
newIndex = Math.max(0, Math.min(slides.length - 1, newIndex));
currentIndex = newIndex;
// включаем transition (класс убрал transition при drag)
setTranslate(-currentIndex * slideWidth);
// релизим pointer capture
try { slider.releasePointerCapture(e.pointerId); } catch (err) { /* ignore */ }
}
slider.addEventListener('pointerup', endDrag);
slider.addEventListener('pointercancel', endDrag);
// предотвращаем перетаскивание изображения/текста браузером
slider.addEventListener('dragstart', (e) => e.preventDefault());
Этот пример — основа. Для продакшена добавьте обработку клавиш, индикаторы, бесшовную цикличность (loop) и ограничения на множественные срабатывания.
Источники
- Создаем слайдер-карусель на чистом JavaScript и CSS без библиотек
- Создаем карусель с drag и swipe функциями на чистом JavaScript
- Карусель (Doka Guide)
- Как сделать слайдер на HTML CSS JS (видео)
- Слайдер на чистом JS с перетаскиванием (видео)
- Слайдер на чистом JS (Drag and Drop) (видео)
Заключение
Резкое (snap) переключение в слайдере достигается двумя путями: нативным CSS scroll-snap для простых случаев и ручной реализацией на JavaScript для тонкой настройки порога, учёта скорости и UX. Главные приёмы в JS: использовать pointerdown/pointermove/pointerup, временно выключать transition при перетаскивании, вычислять порог как долю ширины слайда и учитывать скорость при отпускании — затем включать transition и устанавливать итоговый translateX для snap. Тестируйте чувствительность (threshold/velocity) на реальных устройствах и не забывайте про touch-action и доступность.