Table of Contents
Что такое touchstart, touchmove, touchend?
Эти события нужны для обработки касаний на мобильных устройствах (пальцами).
| Событие | Когда срабатывает |
|---|---|
touchstart | Когда палец касается экрана |
touchmove | Когда палец двигается по экрану |
touchend | Когда палец убирается с экрана |
Они похожи на mousedown, mousemove, mouseup, но для пальцев.
Простой пример
<div id="box" style="width: 200px; height: 200px; background: lightblue;">
Коснись меня
</div>
<script>
const box = document.getElementById('box');
box.addEventListener('touchstart', (event) => {
box.style.background = 'lightgreen';
console.log('Палец коснулся!', event.touches);
});
box.addEventListener('touchmove', (event) => {
box.style.background = 'yellow';
console.log('Палец двигается!', event.touches);
});
box.addEventListener('touchend', (event) => {
box.style.background = 'lightblue';
console.log('Палец убрали!', event.touches);
});
</script>
Что происходит:
- Когда ты касаешься
div→ фон меняется на зелёный. - Двигаешь пальцем → фон жёлтый.
- Убираешь палец → фон снова голубой.
Где это часто используют:
- Swipe (свайпы)
Чтобы определить, например, что пользователь провёл влево или вправо (например, в слайдерах или галереях). - Drag & Drop на мобильных
Когда хочешь сделать перетаскивание элементов пальцем, а не мышкой. - Кастомные жесты
Например, зажатие двумя пальцами для масштабирования (pinch-zoom). - Игры
Чтобы обрабатывать управление движением героя или объектов прикосновением. - Переходы между страницами
Например, свайп влево → перейти на следующую страницу, свайп вправо → вернуться назад.
Маленький пример свайпа вправо/влево
<script>
let startX = 0;
let endX = 0;
document.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
});
document.addEventListener('touchend', (e) => {
endX = e.changedTouches[0].clientX;
handleSwipe();
});
function handleSwipe() {
if (endX - startX > 50) {
console.log('Свайп вправо!');
} else if (startX - endX > 50) {
console.log('Свайп влево!');
} else {
console.log('Свайп слишком короткий');
}
}
</script>
Важно:
event.touches— это список всех пальцев, которые в данный момент касаются экрана.event.changedTouches— это те пальцы, которые изменили состояние (например, убрали палец).
Drag & Drop пальцем на мобильных устройствах
Пример: перетаскиваем квадрат пальцем
<div id="dragMe" style="
width: 100px;
height: 100px;
background: tomato;
position: absolute;
top: 100px;
left: 100px;
touch-action: none; /* важно! чтобы отключить зум/скролл */
">
Тяни меня
</div>
<script>
const box = document.getElementById('dragMe');
let offsetX = 0;
let offsetY = 0;
box.addEventListener('touchstart', (e) => {
const touch = e.touches[0];
const rect = box.getBoundingClientRect();
offsetX = touch.clientX - rect.left;
offsetY = touch.clientY - rect.top;
console.log('Начал тянуть');
});
box.addEventListener('touchmove', (e) => {
e.preventDefault(); // отменяем скролл страницы
const touch = e.touches[0];
box.style.left = (touch.clientX - offsetX) + 'px';
box.style.top = (touch.clientY - offsetY) + 'px';
console.log('Тяну...', touch.clientX, touch.clientY);
});
box.addEventListener('touchend', (e) => {
console.log('Отпустил');
});
</script>
Как это работает:
- Когда палец касается (
touchstart) — запоминаем, где именно на элементе было касание. - Когда двигаем палец (
touchmove) — перемещаем элемент туда, где сейчас палец. - Когда отпускаем палец (
touchend) — можем, например, проверить, куда элемент дотащили.
На что обратить внимание:
touch-action: none;в CSS нужен, чтобы браузер не пытался скроллить или зумить страницу во время касания.e.preventDefault()внутриtouchmove, чтобы отключить стандартное поведение (например, скроллинг).- Работает только на устройствах с тачскрином (но можно допилить поддержку мышки — через
mousedown,mousemove,mouseup).
Пример на проекте Gstaadguy.
function enableDragToClose() {
const nav = document.querySelector(CLASSNAMES.nav);
const mapMobile = document.querySelector(CLASSNAMES.mapMobile);
const $btn = document.querySelector(CLASSNAMES.showMapBtn);
const body = document.body;
if (!nav || !mapMobile) return;
let startY = 0, dragging = false;
nav.style.touchAction = 'pan-x';
nav.style.userSelect = 'none';
nav.addEventListener('touchstart', e => {
startY = e.touches[0].clientY;
dragging = true;
nav.style.transition = 'none';
nav.style.willChange = 'transform, opacity';
}, { passive: true });
nav.addEventListener('touchmove', e => {
if (!dragging) return;
const diffY = e.touches[0].clientY - startY;
if (diffY < 0) nav.style.transform = `translateY(${diffY}px)`;
}, { passive: true });
nav.addEventListener('touchend', () => {
dragging = false;
nav.style.transition = 'transform 1.3s ease 0s, opacity 1.3s ease 0.7s';
const matrix = window.getComputedStyle(nav).transform;
const diffY = matrix !== 'none' ? parseFloat(matrix.split(',')[5]) : 0;
if (diffY < -120) {
nav.style.transform = 'translateY(-100vh)';
nav.style.opacity = '0';
body.style.overflow = '';
body.style.height = '';
$btn.style.display = '';
setTimeout(() => {
nav.classList.remove('active');
nav.style.cssText = ''; // reset inline styles
}, 2000);
setTimeout(() => mapMobile.classList.remove('map-active-mobile'), 800);
} else {
nav.style.transform = 'translateY(0)';
}
});
}
Что делает функция enableDragToClose?
Функция позволяет пальцем тащить вниз навигационное меню, чтобы закрыть его, если провести достаточно сильно вверх (на самом деле тут свайп “вверх”).
Процесс:
- Когда ты тянешь меню вверх пальцем — оно двигается вместе с пальцем.
- Если “потянуть сильно” — меню плавно уходит вверх и исчезает.
- Иначе — возвращается на место.
Важно:
Подготовка для тач-событий:
nav.style.touchAction = 'pan-x';
nav.style.userSelect = 'none';
touchAction: pan-x— разрешает только горизонтальные скроллы (блокирует вертикальные).userSelect: none— запрет выделения текста при касании.
Обработка touchstart:
nav.addEventListener('touchstart', e => { ... });
Когда пользователь касается:
- Запоминаем, где именно по вертикали палец коснулся (
startY). - Включаем флаг
dragging = true— значит, сейчас тащим. - Убираем переходы анимаций (
transition: none), чтобы во время перетаскивания не было тормозов. - Включаем оптимизацию для браузера (
willChange: transform, opacity).
Обработка touchmove:
nav.addEventListener('touchmove', e => { ... });
Пока палец двигается:
- Если
dragging == true, считаем разницу (diffY) — сколько пикселей двинули палец вверх или вниз. - Если тянем вверх (потому что
diffY < 0), двигаем менюtransform: translateY(diffY).
Обработка touchend:
nav.addEventListener('touchend', () => { ... });
Когда отпускаем палец:
- Выключаем
dragging. - Ставим обратно плавные анимации на
transformиopacity. - Читаем реальное текущее смещение через
getComputedStyle(nav).transform.
Проверка: “достаточно ли сильно тянули”:
if (diffY < -120) { ... }
Если меню тянули вверх более чем на 120px:
- Прячем меню (
translateY(-100vh),opacity: 0). - Восстанавливаем скроллинг страницы (
body.style.overflow = ''). - Показываем кнопку карты (
$btn.style.display = ''). - Через 2 секунды сбрасываем все стили навигации (
nav.style.cssText = '') и убираем классactive. - Через 0.8 секунд убираем класс активности с карты (
mapMobile.classList.remove('map-active-mobile')).
Иначе:
- Если тянули слабо — возвращаем меню обратно на место (
translateY(0)).
Кратко: как это выглядит для пользователя
- Открыто навигационное меню на мобильнике.
- Ты пальцем тянешь меню вверх.
- Если тянешь сильно — меню уезжает и закрывается.
- Если тянешь слабо — меню возвращается назад.

Тянем.

Потянули достаточно сильно чтобы меню при отпускании само поехало вверх и закрылось.

И плавно с opacity пропадет.

Зачем нужны некоторые штуки:
willChange: transform, opacity— браузер будет заранее готовиться к анимации → плавность выше.touchAction: pan-x— чтобы отключить вертикальный скролл на устройстве во время перетаскивания меню.getComputedStyle(nav).transform— чтобы узнать, на сколько именно сдвинули элемент (мы же тащили его пальцем).
Можно ли было реализовать эту логику по другому?
Да, конечно, эту логику можно было реализовать по-другому, причём проще и меньше кода.
Вот несколько альтернативных способов, кратко:
1. Через Pointer Events
Вместо touchstart/touchmove/touchend можно использовать один универсальный API:pointerdown, pointermove, pointerup.
Плюсы:
- Работает и на пальце, и на мышке без дополнительного кода.
- Проще тестировать на компьютере.
Минус:
- Старые браузеры могут не поддерживать (но сейчас почти все норм).
2. Через requestAnimationFrame
Вместо того чтобы сразу в touchmove обновлять style.transform,
можно просто сохранять координаты, а плавное обновление делать через requestAnimationFrame.
Плюсы:
- Более плавная анимация.
- Меньше нагрузка на процессор (особенно на слабых телефонах).
Минус:
- Кода чуть больше.
3. Через готовую библиотеку
Например, через маленькую библиотеку типа:
Hammer.jsZingTouch
Они умеют сразу определять свайпы, драги и т.п.
Плюсы:
- Почти ничего не надо писать самому.
- Бонус: можно сразу обрабатывать разные жесты (например, pinch, rotate).
Минус:
- Подключение сторонней библиотеки ради одного свайпа — может быть излишним.
4. Через CSS и touch-action: manipulation + overflow: scroll
Можно было сделать меню с вертикальным скроллом и слушать просто событие scroll:
- Если скролл ушёл достаточно вверх — закрыть меню.
Плюсы:
- Меньше кастомного JavaScript.
- Всё максимально нативно.
Минус:
- Не очень удобно контролировать точную “чувствительность” свайпа.
- Нужно аккуратно настраивать стили.
Как бы я сам сделал для реального проекта?
👉 Я бы выбрал Pointer Events + requestAnimationFrame.
Это даёт:
- Поддержку мыши и пальца одновременно.
- Очень плавную анимацию.
- Меньше ошибок с производительностью.