---
title: Mobile Interaction Patterns
created: 2026-07-03
updated: 2026-07-03
type: system
tags: [mobile, interaction, gesture, touch, swipe, haptics, animation]
sources:
- https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions
- https://developer.mozilla.org/en-US/docs/Web/API/Touch_events
- https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events
- https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
- https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations
confidence: high
---
# Mobile Interaction Patterns — Жесты, тач, анимации
## 1. Touch Events & Gestures
### 1.1 Basic Touch Handling
```javascript
// Touch start — feedback at press
element.addEventListener('touchstart', (e) => {
const touch = e.touches[0];
// Handle initial touch
}, { passive: true });
// Touch move — dragging
element.addEventListener('touchmove', (e) => {
e.preventDefault(); // Prevent scroll during drag
}, { passive: false });
// Touch end — release
element.addEventListener('touchend', (e) => {
// Handle release
});
```
### 1.2 Swipe Detection
```javascript
class SwipeHandler {
constructor(element, options = {}) {
this.element = element;
this.threshold = options.threshold || 50;
this.velocityThreshold = options.velocityThreshold || 0.3;
this.direction = options.direction || 'horizontal';
this.onSwipe = options.onSwipe;
this.startX = 0;
this.startY = 0;
this.startTime = 0;
this.isSwiping = false;
this.bindEvents();
}
bindEvents() {
this.element.addEventListener('touchstart', (e) => {
this.startX = e.touches[0].clientX;
this.startY = e.touches[0].clientY;
this.startTime = Date.now();
this.isSwiping = true;
}, { passive: true });
this.element.addEventListener('touchend', (e) => {
if (!this.isSwiping) return;
this.isSwiping = false;
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const endTime = Date.now();
const diffX = this.startX - endX;
const diffY = this.startY - endY;
const timeDiff = endTime - this.startTime;
const velocity = Math.abs(diffX) / timeDiff;
// Check direction and distance
if (this.direction === 'horizontal' && Math.abs(diffX) > this.threshold) {
if (velocity > this.velocityThreshold) {
const direction = diffX > 0 ? 'left' : 'right';
this.onSwipe?.(direction, diffX, diffY);
}
} else if (this.direction === 'vertical' && Math.abs(diffY) > this.threshold) {
if (velocity > this.velocityThreshold) {
const direction = diffY > 0 ? 'up' : 'down';
this.onSwipe?.(direction, diffX, diffY);
}
}
}, { passive: true });
}
}
// Usage
new SwipeHandler(container, {
threshold: 50,
velocityThreshold: 0.3,
onSwipe: (direction, dx, dy) => {
console.log(`Swiped ${direction} (${dx}, ${dy})`);
}
});
```
### 1.3 Long Press / Double Tap
```javascript
class LongPressHandler {
constructor(element, onLongPress, onDoubleTap) {
this.element = element;
this.longPressThreshold = 500;
this.doubleTapThreshold = 300;
this.timeout = null;
this.tapCount = 0;
this.tapTimer = null;
this.bindEvents();
}
bindEvents() {
this.element.addEventListener('touchstart', (e) => {
// Long press
this.timeout = setTimeout(() => {
this.longPress?.(e.touches[0]);
}, this.longPressThreshold);
// Double tap
this.tapCount++;
clearTimeout(this.tapTimer);
this.tapTimer = setTimeout(() => {
if (this.tapCount === 2) {
this.doubleTap?.(e.changedTouches[0]);
}
this.tapCount = 0;
}, this.doubleTapThreshold);
});
this.element.addEventListener('touchend', () => {
clearTimeout(this.timeout);
});
}
}
```
## 2. Pull to Refresh
### 2.1 CSS + JS Pull to Refresh
```html
```
```css
.pull-to-refresh {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transform: translateY(-100%);
transition: transform 0.3s ease;
z-index: 100;
}
.pull-to-refresh.active {
transform: translateY(0);
}
.ptr-spinner {
width: 24px;
height: 24px;
border: 3px solid var(--border-1);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.ptr-label {
font-size: var(--text-sm);
color: var(--text-secondary);
}
/* Scroll container */
.scroll-container {
padding-top: 60px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
```
```javascript
class PullToRefresh {
constructor(element, onRefresh) {
this.element = element;
this.content = element.querySelector('.scroll-container');
this.refresh = element.querySelector('.pull-to-refresh');
this.onRefresh = onRefresh;
this.startY = 0;
this.currentY = 0;
this.threshold = 80;
this.bindEvents();
}
bindEvents() {
this.content.addEventListener('touchstart', (e) => {
if (this.content.scrollTop === 0) {
this.startY = e.touches[0].clientY;
}
}, { passive: true });
this.content.addEventListener('touchmove', (e) => {
if (this.content.scrollTop === 0) {
this.currentY = e.touches[0].clientY;
const diff = this.currentY - this.startY;
if (diff > 0) {
e.preventDefault();
this.refresh.style.transform = `translateY(${Math.min(diff * 0.5, this.threshold)}px)`;
if (diff >= this.threshold) {
this.refresh.classList.add('active');
}
}
}
}, { passive: false });
this.content.addEventListener('touchend', () => {
if (this.refresh.classList.contains('active')) {
this.refresh.style.transform = 'translateY(0)';
this.refresh.classList.add('active');
this.onRefresh?.(() => {
this.refresh.classList.remove('active');
});
}
});
}
}
// Usage
new PullToRefresh(document.body, (done) => {
fetch('/api/data')
.then(r => r.json())
.then(data => {
updateContent(data);
done();
});
});
```
## 3. Touch Feedback Patterns
### 3.1 Active State (Ripple-like)
```css
.touch-feedback {
position: relative;
overflow: hidden;
transition: transform 0.1s ease, opacity 0.1s ease;
-webkit-tap-highlight-color: transparent;
}
.touch-feedback:active {
transform: scale(0.97);
opacity: 0.8;
}
/* Subtle press effect */
.touch-feedback-press {
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.touch-feedback-press:active {
transform: scale(0.98);
box-shadow: 0 0 0 2px var(--primary);
}
```
### 3.2 Haptic Feedback
```javascript
// Web Vibration API (Android + some iOS)
function vibrate(pattern = [50]) {
if (navigator.vibrate) {
navigator.vibrate(pattern);
}
}
// Haptic patterns for different actions
const haptics = {
light: [16],
medium: [30],
heavy: [50],
success: [30, 50, 30],
error: [100, 50, 100],
selection: [10],
click: [15]
};
// Usage
function handleTouch(element) {
element.addEventListener('touchend', () => {
vibrate(haptics.click);
});
}
```
## 4. Scroll-Linked Animations
### 4.1 Scroll Reveal
```javascript
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
revealObserver.unobserve(entry.target);
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
document.querySelectorAll('.reveal').forEach(el => {
revealObserver.observe(el);
});
```
```css
.reveal {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.revealed {
opacity: 1;
transform: translateY(0);
}
/* Staggered reveal for lists */
.reveal:nth-child(1) { transition-delay: 0ms; }
.reveal:nth-child(2) { transition-delay: 50ms; }
.reveal:nth-child(3) { transition-delay: 100ms; }
.reveal:nth-child(4) { transition-delay: 150ms; }
.reveal:nth-child(5) { transition-delay: 200ms; }
```
### 4.2 Parallax Scroll
```css
.parallax-section {
position: relative;
overflow: hidden;
height: 60vh;
}
.parallax-bg {
position: absolute;
top: -20%;
left: 0;
width: 100%;
height: 140%;
background-size: cover;
will-change: transform;
}
.parallax-content {
position: relative;
z-index: 2;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
```
```javascript
function handleParallax() {
const sections = document.querySelectorAll('.parallax-section');
const scrollY = window.scrollY;
sections.forEach(section => {
const rect = section.getBoundingClientRect();
const offset = rect.top * 0.3;
const bg = section.querySelector('.parallax-bg');
if (bg) {
bg.style.transform = `translateY(${offset}px)`;
}
});
}
window.addEventListener('scroll', handleParallax, { passive: true });
```
### 4.3 Sticky Scroll Sections
```css
.sticky-scroll-container {
display: flex;
height: 300vh;
}
.sticky-panel {
position: sticky;
top: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
/* Sequential reveal */
.sticky-panel:nth-child(1) { background: #f0f0f0; z-index: 1; }
.sticky-panel:nth-child(2) { background: #e0e0e0; z-index: 2; }
.sticky-panel:nth-child(3) { background: #d0d0d0; z-index: 3; }
```
## 5. Gesture Navigation
### 5.1 Swipe Back
```javascript
class SwipeBack {
constructor(element, onSwipeBack) {
this.element = element;
this.onSwipeBack = onSwipeBack;
this.startX = 0;
this.threshold = 100;
this.bindEvents();
}
bindEvents() {
this.element.addEventListener('touchstart', (e) => {
this.startX = e.touches[0].clientX;
}, { passive: true });
this.element.addEventListener('touchmove', (e) => {
const diff = e.touches[0].clientX - this.startX;
if (diff > 0 && window.scrollX === 0) {
e.preventDefault();
// Visual feedback
this.element.style.transform = `translateX(${diff}px)`;
this.element.style.opacity = 1 - (diff / this.threshold);
}
}, { passive: false });
this.element.addEventListener('touchend', (e) => {
const diff = e.changedTouches[0].clientX - this.startX;
if (diff > this.threshold) {
this.onSwipeBack?.();
}
this.element.style.transform = '';
this.element.style.opacity = '';
});
}
}
```
### 5.2 Horizontal Card Swipe
```javascript
class HorizontalSwipe {
constructor(element, options = {}) {
this.element = element;
this.cards = element.querySelectorAll('.card');
this.currentIndex = 0;
this.threshold = 100;
this.onSwipe = options.onSwipe;
this.bindEvents();
}
bindEvents() {
this.cards.forEach(card => {
card.addEventListener('touchstart', (e) => {
this.startX = e.touches[0].clientX;
}, { passive: true });
card.addEventListener('touchend', (e) => {
const diff = e.changedTouches[0].clientX - this.startX;
if (Math.abs(diff) > this.threshold) {
this.currentIndex += diff > 0 ? 1 : -1;
this.currentIndex = Math.max(0, Math.min(this.currentIndex, this.cards.length - 1));
this.onSwipe?.(this.currentIndex, diff > 0 ? 'next' : 'prev');
}
});
});
}
}
```
## 6. Scroll-Linked CSS Animations
### 6.1 Scroll-driven Animations (Modern)
```css
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-on-scroll {
animation: fade-in-up linear;
animation-timeline: view();
animation-range: entry 0% entry 25%;
}
/* Progress bar that fills on scroll */
.progress-track {
height: 4px;
background: var(--surface-2);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--primary);
border-radius: 2px;
animation: progress linear;
animation-timeline: scroll();
animation-range: 0 100vh;
}
@keyframes progress {
to { width: 100%; }
}
```
### 6.2 Scroll-linked Typography
```css
.scroll-text {
font-size: clamp(1rem, 1rem + 20cqw, 3rem);
transition: font-size 0.1s ease;
}
/* Scale with scroll position */
.scroll-scale {
transform: scale(calc(1 + scroll(target, transform) * 0.5));
animation: scroll-scale linear;
animation-timeline: scroll();
}
```
## 7. Touch-Optimized Carousel
```javascript
class TouchCarousel {
constructor(element, options = {}) {
this.element = element;
this.slides = element.querySelectorAll('.slide');
this.currentIndex = 0;
this.startX = 0;
this.currentX = 0;
this.isDragging = false;
this.threshold = 50;
this.autoplay = options.autoplay ?? true;
this.interval = options.interval ?? 5000;
this.timer = null;
this.bindEvents();
this.startAutoplay();
}
bindEvents() {
// Touch
this.element.addEventListener('touchstart', (e) => {
this.stopAutoplay();
this.startX = e.touches[0].clientX;
this.isDragging = true;
this.element.style.transition = 'none';
}, { passive: true });
this.element.addEventListener('touchmove', (e) => {
if (!this.isDragging) return;
this.currentX = e.touches[0].clientX;
const diff = this.currentX - this.startX;
this.slides[this.currentIndex].style.transform = `translateX(${diff}px)`;
}, { passive: true });
this.element.addEventListener('touchend', () => {
this.isDragging = false;
this.element.style.transition = 'transform 0.3s ease';
const diff = this.currentX - this.startX;
if (Math.abs(diff) > this.threshold) {
this.goTo(diff > 0 ? this.currentIndex - 1 : this.currentIndex + 1);
} else {
this.goTo(this.currentIndex);
}
this.startAutoplay();
});
// Indicator dots
this.dots = element.querySelectorAll('.dot');
this.dots.forEach((dot, i) => {
dot.addEventListener('click', () => this.goTo(i));
});
}
goTo(index) {
this.currentIndex = Math.max(0, Math.min(index, this.slides.length - 1));
this.slides[this.currentIndex].style.transform = 'translateX(0)';
this.updateDots();
}
updateDots() {
this.dots.forEach((dot, i) => {
dot.classList.toggle('active', i === this.currentIndex);
});
}
startAutoplay() {
this.timer = setInterval(() => {
this.goTo((this.currentIndex + 1) % this.slides.length);
}, this.interval);
}
stopAutoplay() {
clearInterval(this.timer);
}
}
```
## 8. Performance Tips for Mobile Touch
### 8.1 Passive Event Listeners
```javascript
// Always use passive for scroll-related listeners
document.addEventListener('scroll', handler, { passive: true });
document.addEventListener('touchstart', handler, { passive: true });
```
### 8.2 Debounce Scroll
```javascript
function debounceScroll(fn, delay = 16) {
let ticking = false;
return () => {
if (!ticking) {
requestAnimationFrame(() => {
fn();
ticking = false;
});
ticking = true;
}
};
}
window.addEventListener('scroll', debounceScroll(() => {
// Handle scroll
}, 16));
```
### 8.3 GPU-Accelerated Animations
```css
/* Always use transform + opacity for animations */
.animated-element {
will-change: transform, opacity;
transform: translateZ(0); /* Force GPU layer */
}
/* Avoid animating: width, height, top, left, margin */
```
## 9. Mobile Interaction Anti-Patterns
| ❌ Плохо | ✅ Хорошо |
|----------|-----------|
| Hover-эффекты | Active/pressed states |
| Автоплей видео | Play по тапу пользователя |
| Скрытые жесты | Визуальные подсказки для жестов |
| 100vh секции | 100dvh (dynamic viewport) |
| Тяжелые scroll-анимации | CSS scroll-driven animations |
| Пасивные листенеры | Passive + debounce |
| Только визуальный фидбэк | Haptic + visual feedback |