--- 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 |