feat(mobile): comprehensive mobile web design patterns — 6 new articles
- mobile-patterns.md: Mobile layout & pattern systems (18.5KB) Fluid typography, container queries, safe-area, CSS architecture, touch-optimized components, iOS/Android specifics, testing checklist - mobile-layout-patterns.md: Mobile layout systems (11.5KB) CSS grid, single-column stack, full-bleed hero, card stack, swipeable carousel, split-screen, off-canvas nav, bottom sheet, FAB, component patterns - mobile-navigation-patterns.md: Mobile navigation (9.8KB) Bottom tab bar, top nav, hamburger menu, bottom sheet nav, pill nav, comparison matrix, navigation rules, swipe navigation, breadcrumbs, tab content - mobile-form-patterns.md: Mobile form patterns (10.3KB) Input types, keyboards, floating labels, validation patterns, progressive disclosure, autocomplete, masked inputs, toggle/radio, form anti-patterns - mobile-interaction-patterns.md: Touch & gestures (16.7KB) Swipe detection, long press, pull-to-refresh, touch feedback, scroll-reveal, parallax, sticky scroll, CSS scroll animations, touch carousel, haptic feedback, passive listeners - mobile-performance-patterns.md: Performance optimization (12.3KB) Core Web Vitals (LCP/INP/CLS), LCP optimization, INP debugging, CLS prevention, image optimization, lazy loading, service workers, CSS/JS optimization, network optimization - mobile-landing-patterns.md: Landing page patterns (21.3KB) Section sequencing, hero, social proof, feature cards, CTA patterns (inline/sticky/full-bleed), testimonials, pricing, FAQ accordion, footer, mobile checklist
This commit is contained in:
1 parent
76c4c5b4fe
commit
083f6ba783
7 files changed
+4192
No files matched your search
@@ -0,0 +1,422 @@
|
|||||||
|
---
|
||||||
|
title: Mobile Form Patterns
|
||||||
|
created: 2026-07-03
|
||||||
|
updated: 2026-07-03
|
||||||
|
type: system
|
||||||
|
tags: [mobile, forms, input, validation, UX, conversion]
|
||||||
|
sources:
|
||||||
|
- https://www.formCreatorai.com/mobile-form-design-patterns-maximize-completion
|
||||||
|
- https://subux.pro/guides/article/input-field-types-and-usage
|
||||||
|
- https://askusers.org/en/blog/mobile-form-optimization-completion-rates
|
||||||
|
- https://www.getworkform.com/blog/mobile-form-design-tips
|
||||||
|
- https://tinyform.com/post/mobile-form-statistics-ux-trends
|
||||||
|
- https://www.reform.app/blog/10-mobile-form-design-tips-for-better-ux
|
||||||
|
confidence: high
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mobile Form Patterns — Формы и инпуты для мобильных
|
||||||
|
|
||||||
|
## 1. Mobile Form Statistics
|
||||||
|
|
||||||
|
- **63%** мобильных форм бросают на полпути (мобильная форма для завершения)
|
||||||
|
- Оптимизированные формы увеличивают completion rate до **43%**
|
||||||
|
- Мобильные формы имеют более высокий abandonment rate, чем десктоп
|
||||||
|
- Каждый дополнительный field = 10% снижение completion rate
|
||||||
|
|
||||||
|
## 2. Input Types & Keyboards
|
||||||
|
|
||||||
|
### 2.1 Correct Input Types
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Email — shows @ and . on keyboard -->
|
||||||
|
<input type="email" placeholder="you@example.com">
|
||||||
|
|
||||||
|
<!-- Phone — shows numeric keypad -->
|
||||||
|
<input type="tel" placeholder="+7 (___) ___-__-__">
|
||||||
|
|
||||||
|
<!-- Number — shows numeric keypad with minus -->
|
||||||
|
<input type="number" placeholder="100">
|
||||||
|
|
||||||
|
<!-- URL — shows .com and / on keyboard -->
|
||||||
|
<input type="url" placeholder="https://example.com">
|
||||||
|
|
||||||
|
<!-- Text — default, full keyboard -->
|
||||||
|
<input type="text" placeholder="Иван">
|
||||||
|
|
||||||
|
<!-- Search — shows search button on keyboard -->
|
||||||
|
<input type="search" placeholder="Поиск...">
|
||||||
|
|
||||||
|
<!-- Password — hides characters -->
|
||||||
|
<input type="password" placeholder="Пароль">
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 inputmode for Custom Keyboards
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Numeric keyboard without changing type -->
|
||||||
|
<input inputmode="numeric" pattern="[0-9]*" placeholder="1234">
|
||||||
|
|
||||||
|
<!-- Decimal keyboard -->
|
||||||
|
<input inputmode="decimal" placeholder="123.45">
|
||||||
|
|
||||||
|
<!-- Search keyboard -->
|
||||||
|
<input inputmode="search" placeholder="Искать...">
|
||||||
|
|
||||||
|
<!-- Tel keyboard -->
|
||||||
|
<input inputmode="tel" placeholder="+7 999 123-45-67">
|
||||||
|
|
||||||
|
<!-- Email keyboard -->
|
||||||
|
<input inputmode="email" placeholder="you@example.com">
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Keyboard Hints
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Enter key label -->
|
||||||
|
<input type="text" placeholder="Сообщение" enterkeyhint="send">
|
||||||
|
|
||||||
|
<!-- Search button -->
|
||||||
|
<input type="search" placeholder="Поиск" enterkeyhint="search">
|
||||||
|
|
||||||
|
<!-- Next (for multi-field forms) -->
|
||||||
|
<input type="text" placeholder="Имя" enterkeyhint="next">
|
||||||
|
<input type="text" placeholder="Email" enterkeyhint="next">
|
||||||
|
<input type="text" placeholder="Телефон" enterkeyhint="done">
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Form Layout Patterns
|
||||||
|
|
||||||
|
### 3.1 Single-Column Form
|
||||||
|
|
||||||
|
```css
|
||||||
|
.mobile-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Floating Label Pattern
|
||||||
|
|
||||||
|
```css
|
||||||
|
.form-group {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 16px 12px 8px;
|
||||||
|
border: 1px solid var(--border-1);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface-1);
|
||||||
|
font-size: 16px; /* Prevents iOS zoom */
|
||||||
|
width: 100%;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus ~ .form-label,
|
||||||
|
.form-input:not(:placeholder-shown) ~ .form-label {
|
||||||
|
top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Multi-Field Row (When Appropriate)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row-3 {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Правила:**
|
||||||
|
- Только для связанных полей (имя/отчество, город/индекс)
|
||||||
|
- Всегда single-column на экранах < 360px
|
||||||
|
- Never for unrelated fields
|
||||||
|
|
||||||
|
## 4. Validation Patterns
|
||||||
|
|
||||||
|
### 4.1 Real-Time Validation
|
||||||
|
|
||||||
|
```css
|
||||||
|
.form-input.valid {
|
||||||
|
border-color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input.invalid {
|
||||||
|
border-color: #ef4444;
|
||||||
|
animation: shake 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ef4444;
|
||||||
|
margin-top: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-success {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #22c55e;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
25% { transform: translateX(-4px); }
|
||||||
|
75% { transform: translateX(4px); }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Inline Validation on Blur
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Validate on blur, not on every keystroke
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
const value = input.value;
|
||||||
|
if (!validate(value)) {
|
||||||
|
input.classList.add('invalid');
|
||||||
|
showError(input, getMessage(value));
|
||||||
|
} else {
|
||||||
|
input.classList.remove('invalid');
|
||||||
|
input.classList.add('valid');
|
||||||
|
showSuccess(input);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Validation Error Summary
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Show at top of form -->
|
||||||
|
<div class="form-error-summary" role="alert">
|
||||||
|
<p>Исправьте следующие ошибки:</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#email">Введите корректный email</a></li>
|
||||||
|
<li><a href="#phone">Номер телефона должен содержать 10 цифр</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Form UX Patterns
|
||||||
|
|
||||||
|
### 5.1 Progressive Disclosure (Step by Step)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.form-step {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-step.active {
|
||||||
|
display: block;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-progress {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-progress__step {
|
||||||
|
flex: 1;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
margin: 0 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-progress__step.active {
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Autocomplete & Autofill
|
||||||
|
|
||||||
|
```html
|
||||||
|
<input type="text" name="fname" autocomplete="given-name">
|
||||||
|
<input type="text" name="lname" autocomplete="family-name">
|
||||||
|
<input type="email" name="email" autocomplete="email">
|
||||||
|
<input type="tel" name="phone" autocomplete="tel">
|
||||||
|
<input type="text" name="city" autocomplete="address-level2">
|
||||||
|
<input type="text" name="postal" autocomplete="postal-code">
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Masked Inputs
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Phone mask
|
||||||
|
function formatPhone(value) {
|
||||||
|
const cleaned = value.replace(/\D/g, '');
|
||||||
|
const match = cleaned.match(/^(\d)(\d{0,3})(\d{0,3})(\d{0,2})(\d{0,2})$/);
|
||||||
|
if (match) {
|
||||||
|
return '+7' + (match[2] ? ' (' + match[2] : '') +
|
||||||
|
(match[3] ? ') ' + match[3] : '') +
|
||||||
|
(match[4] ? '-' + match[4] : '') +
|
||||||
|
(match[5] ? '-' + match[5] : '');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Form Component Patterns
|
||||||
|
|
||||||
|
### 6.1 Select/Picker
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Use native select for mobile — triggers OS picker -->
|
||||||
|
<select name="country">
|
||||||
|
<option value="">Выберите страну</option>
|
||||||
|
<option value="ru">Россия</option>
|
||||||
|
<option value="by">Беларусь</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- Custom picker for mobile -->
|
||||||
|
<div class="picker-trigger">
|
||||||
|
<span class="picker-label">Выберите дату</span>
|
||||||
|
<span class="picker-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="picker-overlay">
|
||||||
|
<div class="picker-wheel">
|
||||||
|
<div class="picker-scroll">
|
||||||
|
<!-- OS-style scroll wheel -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Checkbox & Radio
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Large touch targets */
|
||||||
|
.form-checkbox,
|
||||||
|
.form-radio {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px solid var(--border-1);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox::after,
|
||||||
|
.form-radio::after {
|
||||||
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--primary);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox.checked::after,
|
||||||
|
.form-radio.checked::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Toggle Switch
|
||||||
|
|
||||||
|
```css
|
||||||
|
.toggle {
|
||||||
|
position: relative;
|
||||||
|
width: 52px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.active {
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.active::after {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Form Anti-Patterns
|
||||||
|
|
||||||
|
| ❌ Плохо | ✅ Хорошо |
|
||||||
|
|----------|-----------|
|
||||||
|
| Автофокус на первом поле | Автофокус только на 1 поле за шаг |
|
||||||
|
| Валидация при вводе | Валидация при blur или submit |
|
||||||
|
| Email type для всех текстовых полей | Правильный inputtype + inputmode |
|
||||||
|
| Фиксированная клавиатура | Динамическая клавиатура |
|
||||||
|
| 10+ полей в одну страницу | Прогрессивная подача (step-by-step) |
|
||||||
|
| Нет подсказок для форматов | Placeholder + подсказки |
|
||||||
|
| Только красная рамка при ошибке | Иконка + текст + анимация |
|
||||||
|
| Сложные пароли без указаний | Показ требований к паролю |
|
||||||
|
|
||||||
|
## 8. Mobile Form Checklist
|
||||||
|
|
||||||
|
| Category | Check |
|
||||||
|
|----------|-------|
|
||||||
|
| **Input Type** | Правильный type + inputmode |
|
||||||
|
| **Keyboard** | Правильная клавиатура для каждого поля |
|
||||||
|
| **Touch Targets** | Минимум 44×44px |
|
||||||
|
| **Labels** | Видимые labels (floating или above) |
|
||||||
|
| **Validation** | Inline, после blur, summary at top |
|
||||||
|
| **Autocomplete** | autocomplete attributes set |
|
||||||
|
| **Progress** | Step indicator для многостраничных |
|
||||||
|
| **Submit** | CTA всегда виден |
|
||||||
|
| **Feedback** | Success/error states |
|
||||||
|
| **Zoom** | 16px font-size на iOS |
|
||||||
@@ -0,0 +1,704 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
<div class="pull-to-refresh">
|
||||||
|
<div class="ptr-spinner">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
<div class="ptr-label">Обновление...</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<!-- Page content -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```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 |
|
||||||
@@ -0,0 +1,923 @@
|
|||||||
|
---
|
||||||
|
title: Mobile Landing Page Patterns
|
||||||
|
created: 2026-07-03
|
||||||
|
updated: 2026-07-03
|
||||||
|
type: system
|
||||||
|
tags: [mobile, landing, conversion, CTA, sections, mobile-first]
|
||||||
|
sources:
|
||||||
|
- https://www.webstacks.com/blog/mobile-website-design-best-practices
|
||||||
|
- https://www.browserstack.com/guide/how-to-implement-mobile-first-design
|
||||||
|
- https://www.engagecoders.com/responsive-web-design-mobile-first-development-best-practices-2025-guide/
|
||||||
|
confidence: high
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mobile Landing Page Patterns — Лендинги для мобильных
|
||||||
|
|
||||||
|
## 1. Mobile Landing Page Structure
|
||||||
|
|
||||||
|
### 1.1 Секвенция секций
|
||||||
|
|
||||||
|
Оптимальный порядок для мобильных лендингов (сверху вниз):
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Hero (с CTA)
|
||||||
|
2. Social Proof (логотипы клиентов)
|
||||||
|
3. Problem/Solution
|
||||||
|
4. Features/Benefits
|
||||||
|
5. How It Works
|
||||||
|
6. Testimonials
|
||||||
|
7. Pricing
|
||||||
|
8. FAQ
|
||||||
|
9. Final CTA
|
||||||
|
10. Footer
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Hero Section (Mobile)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="mobile-hero" style="min-height: 80dvh;">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="hero-title">Заголовок, который решает проблему</h1>
|
||||||
|
<p class="hero-subtitle">Подзаголовок в 1-2 предложения, объясняющий ценность</p>
|
||||||
|
<div class="hero-cta-group">
|
||||||
|
<a href="#signup" class="btn btn-primary">Начать бесплатно</a>
|
||||||
|
<a href="#demo" class="btn btn-ghost">Смотреть демо ▶</a>
|
||||||
|
</div>
|
||||||
|
<div class="hero-social-proof">
|
||||||
|
<div class="avatars">
|
||||||
|
<img src="/av1.jpg" alt="" class="avatar">
|
||||||
|
<img src="/av2.jpg" alt="" class="avatar">
|
||||||
|
<img src="/av3.jpg" alt="" class="avatar">
|
||||||
|
</div>
|
||||||
|
<span class="social-text">+2,000 уже используют</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.mobile-hero {
|
||||||
|
padding: calc(24px + var(--safe-top)) 16px calc(var(--safe-bottom) + 24px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 80dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: clamp(2rem, 8vw, 3.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: clamp(1.1rem, 3.5vw, 1.25rem);
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cta-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-social-proof {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatars {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--surface-1);
|
||||||
|
margin-left: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-text {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Social Proof Section
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="social-proof">
|
||||||
|
<p class="section-label">Нам доверяют</p>
|
||||||
|
<div class="logo-grid">
|
||||||
|
<img src="/logo1.svg" alt="Company" class="logo">
|
||||||
|
<img src="/logo2.svg" alt="Company" class="logo">
|
||||||
|
<img src="/logo3.svg" alt="Company" class="logo">
|
||||||
|
<img src="/logo4.svg" alt="Company" class="logo">
|
||||||
|
<img src="/logo5.svg" alt="Company" class="logo">
|
||||||
|
</div>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-value">10K+</span>
|
||||||
|
<span class="stat-label">Клиентов</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-value">99.9%</span>
|
||||||
|
<span class="stat-label">Uptime</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-value">4.9/5</span>
|
||||||
|
<span class="stat-label">Рейтинг</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.social-proof {
|
||||||
|
padding: 48px 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-grid {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 24px;
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(1);
|
||||||
|
transition: opacity 0.2s, filter 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo:hover,
|
||||||
|
.logo:active {
|
||||||
|
opacity: 1;
|
||||||
|
filter: grayscale(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Feature/Benefit Sections
|
||||||
|
|
||||||
|
### 2.1 Feature Card (Mobile)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="features">
|
||||||
|
<h2 class="section-title">Всё что нужно для роста</h2>
|
||||||
|
<div class="feature-list">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">📊</div>
|
||||||
|
<h3 class="feature-title">Аналитика</h3>
|
||||||
|
<p class="feature-desc">Мгновенные дашборды с ключевыми метриками</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">⚡</div>
|
||||||
|
<h3 class="feature-title">Скорость</h3>
|
||||||
|
<p class="feature-desc">Загрузка менее чем за 1 секунду</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">🔒</div>
|
||||||
|
<h3 class="feature-title">Безопасность</h3>
|
||||||
|
<p class="feature-desc">Шифрование данных и соответствие GDPR</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.features {
|
||||||
|
padding: 48px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: clamp(1.5rem, 6vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--surface-1);
|
||||||
|
border: 1px solid var(--border-1);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:active {
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-title {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-desc {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Feature Image + Text (Alternating)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="feature-showcase">
|
||||||
|
<div class="feature-showcase__content">
|
||||||
|
<h3 class="feature-showcase__title">Быстрый старт за 2 минуты</h3>
|
||||||
|
<p class="feature-showcase__desc">Никаких сложных настроек. Подключите API и начните работу.</p>
|
||||||
|
<a href="#docs" class="btn btn-primary">Документация →</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-showcase__media">
|
||||||
|
<img src="/screenshot1.webp" alt="Dashboard preview" loading="lazy">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="feature-showcase reversed">
|
||||||
|
<div class="feature-showcase__content">
|
||||||
|
<h3 class="feature-showcase__title">Автоматизация процессов</h3>
|
||||||
|
<p class="feature-showcase__desc">Настройте один раз — система будет работать за вас.</p>
|
||||||
|
<a href="#features" class="btn btn-primary">Узнать больше →</a>
|
||||||
|
</div>
|
||||||
|
<div class="feature-showcase__media">
|
||||||
|
<img src="/screenshot2.webp" alt="Automation preview" loading="lazy">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.feature-showcase {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 48px 16px;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-showcase.reversed .feature-showcase__content {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-showcase.reversed .feature-showcase__media {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-showcase__title {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-showcase__desc {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-showcase__media {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-showcase__media img {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.feature-showcase {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 48px;
|
||||||
|
padding: 64px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-showcase.reversed .feature-showcase__content {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-showcase.reversed .feature-showcase__media {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-showcase__content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-showcase__media {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. CTA Patterns for Mobile
|
||||||
|
|
||||||
|
### 3.1 Inline CTA
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="cta-inline">
|
||||||
|
<div class="cta-inline__content">
|
||||||
|
<h3 class="cta-inline__title">Готовы начать?</h3>
|
||||||
|
<p class="cta-inline__desc">Присоединяйтесь к 10,000+ компаний, которые уже используют наш сервис</p>
|
||||||
|
</div>
|
||||||
|
<a href="#signup" class="btn btn-primary btn-large">Создать аккаунт</a>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.cta-inline {
|
||||||
|
padding: 48px 16px;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border-radius: 16px;
|
||||||
|
margin: 0 16px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-inline__title {
|
||||||
|
font-size: clamp(1.25rem, 5vw, 1.75rem);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-inline__desc {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 56px;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Sticky Bottom CTA
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="sticky-cta">
|
||||||
|
<a href="#signup" class="btn btn-primary sticky-cta__btn">
|
||||||
|
Начать бесплатно
|
||||||
|
</a>
|
||||||
|
<span class="sticky-cta__hint">Без привязки карты</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.sticky-cta {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
padding-bottom: calc(12px + var(--safe-bottom));
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-top: 1px solid var(--border-1);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
box-shadow: 0 -4px 12px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-cta__btn {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 56px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-cta__hint {
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Full-Bleed CTA
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="cta-full-bleed">
|
||||||
|
<h2 class="cta-full-bleed__title">Готовы трансформировать бизнес?</h2>
|
||||||
|
<p class="cta-full-bleed__desc">Запросите демо за 2 минуты</p>
|
||||||
|
<a href="#demo" class="btn btn-white btn-large">Запросить демо</a>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.cta-full-bleed {
|
||||||
|
padding: 64px 24px;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-full-bleed__title {
|
||||||
|
font-size: clamp(1.5rem, 6vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-full-bleed__desc {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-white {
|
||||||
|
background: white;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Testimonials for Mobile
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="testimonials">
|
||||||
|
<h2 class="section-title">Что говорят клиенты</h2>
|
||||||
|
<div class="testimonial-scroll">
|
||||||
|
<div class="testimonial-card">
|
||||||
|
<p class="testimonial-text">"Этот сервис изменил наш подход к аналитике. Всё работает как часы."</p>
|
||||||
|
<div class="testimonial-author">
|
||||||
|
<img src="/av1.jpg" alt="Иван П." class="testimonial-avatar">
|
||||||
|
<div>
|
||||||
|
<p class="testimonial-name">Иван Петров</p>
|
||||||
|
<p class="testimonial-role">CEO, TechCorp</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="testimonial-card">
|
||||||
|
<p class="testimonial-text">"Наконец-то удобный интерфейс, который не ломается на телефоне."</p>
|
||||||
|
<div class="testimonial-author">
|
||||||
|
<img src="/av2.jpg" alt="Анна С." class="testimonial-avatar">
|
||||||
|
<div>
|
||||||
|
<p class="testimonial-name">Анна Сидорова</p>
|
||||||
|
<p class="testimonial-role">CMO, Digital Agency</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.testimonial-scroll {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-scroll::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-card {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: min(340px, 85vw);
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--surface-1);
|
||||||
|
border: 1px solid var(--border-1);
|
||||||
|
border-radius: 12px;
|
||||||
|
scroll-snap-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-text {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-author {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-role {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Pricing for Mobile
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="pricing">
|
||||||
|
<h2 class="section-title">Простые тарифы</h2>
|
||||||
|
|
||||||
|
<div class="pricing-toggle">
|
||||||
|
<span class="pricing-label active">Ежемесячно</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="pricing-toggle">
|
||||||
|
<span class="switch-slider"></span>
|
||||||
|
</label>
|
||||||
|
<span class="pricing-label">Ежегодно (-20%)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pricing-cards">
|
||||||
|
<div class="pricing-card">
|
||||||
|
<h3 class="pricing-card__name">Старт</h3>
|
||||||
|
<div class="pricing-card__price">
|
||||||
|
<span class="price-monthly">₽990</span>
|
||||||
|
<span class="price-yearly" style="display:none;">₽790</span>
|
||||||
|
<span class="price-period">/мес</span>
|
||||||
|
</div>
|
||||||
|
<ul class="pricing-card__features">
|
||||||
|
<li>✓ До 1,000 контактов</li>
|
||||||
|
<li>✓ Базовая аналитика</li>
|
||||||
|
<li>✓ Email поддержка</li>
|
||||||
|
</ul>
|
||||||
|
<a href="#signup" class="btn btn-secondary pricing-card__btn">Начать</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pricing-card featured">
|
||||||
|
<div class="pricing-card__badge">Популярный</div>
|
||||||
|
<h3 class="pricing-card__name">Бизнес</h3>
|
||||||
|
<div class="pricing-card__price">
|
||||||
|
<span class="price-monthly">₽2,990</span>
|
||||||
|
<span class="price-yearly" style="display:none;">₽2,390</span>
|
||||||
|
<span class="price-period">/мес</span>
|
||||||
|
</div>
|
||||||
|
<ul class="pricing-card__features">
|
||||||
|
<li>✓ Безлимит контактов</li>
|
||||||
|
<li>✓ Продвинутая аналитика</li>
|
||||||
|
<li>✓ Приоритетная поддержка</li>
|
||||||
|
<li>✓ API доступ</li>
|
||||||
|
</ul>
|
||||||
|
<a href="#signup" class="btn btn-primary pricing-card__btn">Выбрать</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.pricing {
|
||||||
|
padding: 48px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
width: 48px;
|
||||||
|
height: 28px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-slider {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border-radius: 14px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-slider::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 2px;
|
||||||
|
top: 2px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input:checked + .switch-slider {
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input:checked + .switch-slider::after {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-cards {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-card {
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--surface-1);
|
||||||
|
border: 1px solid var(--border-1);
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-card.featured {
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 2px var(--primary);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-card__badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -12px;
|
||||||
|
right: 16px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-card__price {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-monthly {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-yearly {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-card__features {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-card__features li {
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid var(--border-1);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-card__features li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-card__btn {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. FAQ for Mobile
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="faq">
|
||||||
|
<h2 class="section-title">Частые вопросы</h2>
|
||||||
|
<div class="accordion">
|
||||||
|
<details class="accordion-item">
|
||||||
|
<summary class="accordion-item__header">
|
||||||
|
<span>Как начать использовать сервис?</span>
|
||||||
|
<span class="accordion-icon">+</span>
|
||||||
|
</summary>
|
||||||
|
<div class="accordion-item__content">
|
||||||
|
<p>Регистрация занимает 2 минуты. Просто укажите email, создайте пароль и следуйте инструкциям на экране.</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<details class="accordion-item">
|
||||||
|
<summary class="accordion-item__header">
|
||||||
|
<span>Можно ли отменить подписку?</span>
|
||||||
|
<span class="accordion-icon">+</span>
|
||||||
|
</summary>
|
||||||
|
<div class="accordion-item__content">
|
||||||
|
<p>Да, вы можете отменить подписку в любой момент из настроек аккаунта. Никаких скрытых платежей.</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<details class="accordion-item">
|
||||||
|
<summary class="accordion-item__header">
|
||||||
|
<span>Есть ли бесплатный пробный период?</span>
|
||||||
|
<span class="accordion-icon">+</span>
|
||||||
|
</summary>
|
||||||
|
<div class="accordion-item__content">
|
||||||
|
<p>Да, 14 дней бесплатного использования всех функций. Без привязки карты.</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.faq {
|
||||||
|
padding: 48px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-item {
|
||||||
|
background: var(--surface-1);
|
||||||
|
border: 1px solid var(--border-1);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-item__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: 600;
|
||||||
|
list-style: none;
|
||||||
|
min-height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-item__header::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-item[open] .accordion-icon {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-item__content {
|
||||||
|
padding: 0 20px 16px;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Footer for Mobile
|
||||||
|
|
||||||
|
```html
|
||||||
|
<footer class="mobile-footer">
|
||||||
|
<div class="footer-links">
|
||||||
|
<a href="/about">О нас</a>
|
||||||
|
<a href="/blog">Блог</a>
|
||||||
|
<a href="/careers">Карьера</a>
|
||||||
|
<a href="/privacy">Конфиденциальность</a>
|
||||||
|
<a href="/terms">Условия</a>
|
||||||
|
<a href="/contact">Контакты</a>
|
||||||
|
</div>
|
||||||
|
<p class="footer-copyright">© 2026 Company. Все права защищены.</p>
|
||||||
|
</footer>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.mobile-footer {
|
||||||
|
padding: 48px 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
min-height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-copyright {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Mobile Landing Checklist
|
||||||
|
|
||||||
|
| Category | Check |
|
||||||
|
|----------|-------|
|
||||||
|
| **Hero** | Заголовок + подзаголовок + CTA + social proof |
|
||||||
|
| **CTA** | Минимум 2 CTA, sticky bottom CTA |
|
||||||
|
| **Features** | Карточки с иконками, скриншоты чередуются |
|
||||||
|
| **Social Proof** | Логотипы клиентов, статистика, отзывы |
|
||||||
|
| **Pricing** | Переключатель месяц/год, 2-3 тарифа |
|
||||||
|
| **FAQ** | Аккордеон, 4-6 вопросов |
|
||||||
|
| **Performance** | LCP < 2.5s, lazy loading, сжатие |
|
||||||
|
| **Navigation** | Sticky nav или bottom tab bar |
|
||||||
|
| **Footer** | Ссылки, копирайт |
|
||||||
|
| **Testing** | iPhone + Android, реальные устройства |
|
||||||
@@ -0,0 +1,570 @@
|
|||||||
|
---
|
||||||
|
title: Mobile Layout Systems
|
||||||
|
created: 2026-07-03
|
||||||
|
updated: 2026-07-03
|
||||||
|
type: system
|
||||||
|
tags: [mobile, layout, system, grid, container-queries, responsive]
|
||||||
|
sources:
|
||||||
|
- https://www.uxpin.com/studio/blog/best-practices-examples-of-excellent-responsive-design/
|
||||||
|
- https://www.startdesigns.com/blog/responsive-web-design-2026/
|
||||||
|
- https://www.jamesrossjr.com/blog/responsive-web-design-best-practices
|
||||||
|
- https://scrimba.com/articles/responsive-web-design-a-complete-guide-2026-2/
|
||||||
|
- https://dev.to/gerryleonugroho/responsive-design-breakpoints-2025-playbook-53ih
|
||||||
|
confidence: high
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mobile Layout Systems — Системы мобильной верстки
|
||||||
|
|
||||||
|
## 1. Mobile Grid Architecture
|
||||||
|
|
||||||
|
### Fluid Grid с Container Queries
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Container query grid — компонент знает свой размер */
|
||||||
|
.grid-container {
|
||||||
|
container-type: inline-size;
|
||||||
|
container-name: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container grid (min-width: 300px) {
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container grid (min-width: 600px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container grid (min-width: 900px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mobile-First CSS Grid Pattern
|
||||||
|
|
||||||
|
```css
|
||||||
|
.mobile-grid {
|
||||||
|
/* Mobile: single column */
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet landscape */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.mobile-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.mobile-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 32px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Mobile Layout Patterns
|
||||||
|
|
||||||
|
### 2.1 Single-Column Stack (Mobile Base)
|
||||||
|
|
||||||
|
Основной паттерн для мобильных — контент идет вертикально.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.mobile-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 20px 16px 100px; /* 100px bottom для sticky CTA */
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-stack .section {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Применение:** Главная страница лендинга, product pages, blog posts.
|
||||||
|
|
||||||
|
### 2.2 Full-Bleed Hero (Mobile)
|
||||||
|
|
||||||
|
На мобайле hero часто занимает весь экран.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.mobile-hero {
|
||||||
|
height: 100dvh; /* dynamic viewport height */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
padding-bottom: calc(var(--safe-bottom) + 24px);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-hero__title {
|
||||||
|
font-size: clamp(2rem, 8vw, 4rem);
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-hero__subtitle {
|
||||||
|
font-size: clamp(1rem, 4vw, 1.25rem);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Card Stack (Mobile Feed)
|
||||||
|
|
||||||
|
Классический паттерн соцсетей и маркетплейсов.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.card-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
padding-bottom: calc(var(--safe-bottom) + 80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-stack .card {
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-stack .card__image {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-stack .card__content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 Swipeable Carousel (Mobile)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.swipe-carousel {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-carousel::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-carousel .carousel-item {
|
||||||
|
scroll-snap-align: start;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: min(280px, 85vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination dots */
|
||||||
|
.carousel-dots {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--text-secondary);
|
||||||
|
opacity: 0.4;
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-dot.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.3);
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 Split Screen (Tablet/Desktop)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.split-screen {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-screen .split-content,
|
||||||
|
.split-screen .split-media {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-screen .split-media {
|
||||||
|
order: -1; /* media first on mobile */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.split-screen {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-screen .split-content,
|
||||||
|
.split-screen .split-media {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
padding: 48px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-screen .split-media {
|
||||||
|
order: 1; /* media right on desktop */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.6 Off-Canvas Navigation
|
||||||
|
|
||||||
|
```css
|
||||||
|
.offcanvas-nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 85vw;
|
||||||
|
max-width: 320px;
|
||||||
|
height: 100dvh;
|
||||||
|
background: var(--surface-1);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 24px;
|
||||||
|
padding-top: calc(24px + var(--safe-top));
|
||||||
|
}
|
||||||
|
|
||||||
|
.offcanvas-nav.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.offcanvas-nav__overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offcanvas-nav.open + .offcanvas-nav__overlay {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.7 Bottom Sheet Pattern
|
||||||
|
|
||||||
|
```css
|
||||||
|
.bottom-sheet {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 16px;
|
||||||
|
padding-bottom: calc(16px + var(--safe-bottom));
|
||||||
|
max-height: 80dvh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-sheet.open {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-sheet__handle {
|
||||||
|
width: 40px;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--border-1);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.8 Floating Action Button (FAB)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.fab {
|
||||||
|
position: fixed;
|
||||||
|
bottom: calc(var(--safe-bottom) + 24px);
|
||||||
|
right: 24px;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
|
z-index: 100;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab:active {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Mobile Typography & Spacing System
|
||||||
|
|
||||||
|
### Fluid Typography
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* Clamp-based fluid type scale */
|
||||||
|
--text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
|
||||||
|
--text-sm: clamp(0.875rem, 0.8rem + 0.3vw, 1rem);
|
||||||
|
--text-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
|
||||||
|
--text-lg: clamp(1.125rem, 1rem + 0.6vw, 1.25rem);
|
||||||
|
--text-xl: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem);
|
||||||
|
--text-2xl: clamp(1.5rem, 1.2rem + 1.5vw, 2rem);
|
||||||
|
--text-3xl: clamp(1.875rem, 1.4rem + 2.3vw, 2.5rem);
|
||||||
|
--text-4xl: clamp(2.25rem, 1.6rem + 3.25vw, 3rem);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mobile Spacing Scale
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--space-xs: 4px;
|
||||||
|
--space-sm: 8px;
|
||||||
|
--space-md: 16px;
|
||||||
|
--space-lg: 24px;
|
||||||
|
--space-xl: 32px;
|
||||||
|
--space-2xl: 48px;
|
||||||
|
--space-3xl: 64px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Mobile Component Patterns
|
||||||
|
|
||||||
|
### Mobile Navigation Bar (Bottom Tab)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.bottom-nav {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: calc(56px + var(--safe-bottom));
|
||||||
|
padding-bottom: var(--safe-bottom);
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-top: 1px solid var(--border-1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav__item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav__item.active {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav__icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mobile Card
|
||||||
|
|
||||||
|
```css
|
||||||
|
.mobile-card {
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-card__image {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-card__content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-card__title {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-card__meta {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mobile Button
|
||||||
|
|
||||||
|
```css
|
||||||
|
.mobile-button {
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, transform 0.1s;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-button:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-button--primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-button--secondary {
|
||||||
|
background: var(--surface-2);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-button--ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Mobile Performance Optimization
|
||||||
|
|
||||||
|
### Image Optimization Pattern
|
||||||
|
|
||||||
|
```html
|
||||||
|
<picture>
|
||||||
|
<source srcset="image-480.webp 480w, image-800.webp 800w, image-1200.webp 1200w"
|
||||||
|
media="(min-width: 768px)">
|
||||||
|
<img
|
||||||
|
src="image-480.webp"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
width="800"
|
||||||
|
height="600"
|
||||||
|
alt="Description"
|
||||||
|
class="lazy-image"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Optimization
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Mobile-first critical CSS (inlined in <head>) */
|
||||||
|
/* Target: < 14KB gzipped for mobile LCP */
|
||||||
|
|
||||||
|
@layer mobile, tablet, desktop;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lazy Loading Pattern
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const lazyImages = document.querySelectorAll('.lazy-image');
|
||||||
|
const lazyObserver = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const img = entry.target;
|
||||||
|
img.src = img.dataset.src || img.src;
|
||||||
|
img.classList.add('loaded');
|
||||||
|
lazyObserver.unobserve(img);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
rootMargin: '200px 0px',
|
||||||
|
threshold: 0.01
|
||||||
|
});
|
||||||
|
|
||||||
|
lazyImages.forEach(img => lazyObserver.observe(img));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Mobile Testing Checklist
|
||||||
|
|
||||||
|
| Category | Check | Tool |
|
||||||
|
|----------|-------|------|
|
||||||
|
| **Layout** | Single-column flow at 320px | Chrome DevTools |
|
||||||
|
| **Touch** | All tap targets ≥ 44×44px | Lighthouse |
|
||||||
|
| **Typography** | No zoom on iOS input focus | Real device |
|
||||||
|
| **Safe Area** | No overlap with notch/home indicator | iPhone real device |
|
||||||
|
| **Performance** | LCP < 2.5s, INP < 200ms, CLS < 0.1 | PageSpeed Insights |
|
||||||
|
| **Scroll** | Horizontal scrolls fixed, vertical smooth | Real device |
|
||||||
|
| **Forms** | Correct keyboard types | Lighthouse |
|
||||||
|
| **Dark Mode** | All components readable | DevTools |
|
||||||
|
| **Orientation** | Portrait & landscape | Real device |
|
||||||
|
|
||||||
|
## 7. Mobile Anti-Patterns
|
||||||
|
|
||||||
|
| ❌ Плохо | ✅ Хорошо |
|
||||||
|
|----------|-----------|
|
||||||
|
| Hamburger-меню для основных навигаций | Bottom tab bar для 3-5 разделов |
|
||||||
|
| Hover-эффекты | Active/pressed states |
|
||||||
|
| Фиксированные ширины | Fluid + container queries |
|
||||||
|
| Полноценные изображения | Responsive images + lazy loading |
|
||||||
|
| Popup-формы на мобайле | Inline-формы |
|
||||||
|
| 100vh для full-height | 100dvh |
|
||||||
|
| Скрытые CTA | Sticky bottom CTA |
|
||||||
|
| Автоплей видео | Play/pause по тапу |
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
---
|
||||||
|
title: Mobile Navigation Patterns
|
||||||
|
created: 2026-07-03
|
||||||
|
updated: 2026-07-03
|
||||||
|
type: system
|
||||||
|
tags: [mobile, navigation, ux, bottom-nav, hamburger, gesture]
|
||||||
|
sources:
|
||||||
|
- https://www.uxpin.com/studio/blog/mobile-navigation-examples/
|
||||||
|
- https://www.uxpin.com/studio/blog/mobile-navigation-patterns-pros-and-cons/
|
||||||
|
- https://www.seven-squar-tech.com/which-mobile-navigation-patterns-works-what-fails/
|
||||||
|
- https://claritee.io/blog/creative-mobile-ui-patterns-for-enhanced-navigation/
|
||||||
|
confidence: high
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mobile Navigation Patterns — Мобильная навигация
|
||||||
|
|
||||||
|
## 1. Типы навигации для мобильных
|
||||||
|
|
||||||
|
### 1.1 Bottom Tab Bar (Наиболее предпочтительная)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.bottom-nav {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: calc(56px + var(--safe-bottom));
|
||||||
|
padding-bottom: var(--safe-bottom);
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-top: 1px solid var(--border-1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav__item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
min-width: 48px;
|
||||||
|
min-height: 48px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav__item.active {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav__item.active .bottom-nav__icon {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav__icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Правила:**
|
||||||
|
- 3-5 элементов максимум
|
||||||
|
- Иконка + подпись
|
||||||
|
- Текущий раздел выделен
|
||||||
|
- Не скрывать при скролле
|
||||||
|
- Лучше чем hamburger (все элементы видны)
|
||||||
|
|
||||||
|
### 1.2 Top Navigation Bar
|
||||||
|
|
||||||
|
```css
|
||||||
|
.top-nav {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 56px;
|
||||||
|
padding: 0 16px;
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-bottom: 1px solid var(--border-1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-nav__title {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-nav__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-nav__action {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-nav__action:active {
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Правила:**
|
||||||
|
- Sticky при скролле (не fixed)
|
||||||
|
- Логотип слева, действия справа
|
||||||
|
- Заголовок по центру
|
||||||
|
- Минимум 2-3 действия
|
||||||
|
|
||||||
|
### 1.3 Hamburger Menu (Slide-in Drawer)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.hamburger-menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 85vw;
|
||||||
|
max-width: 320px;
|
||||||
|
height: 100dvh;
|
||||||
|
background: var(--surface-1);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 24px;
|
||||||
|
padding-top: calc(24px + var(--safe-top));
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-menu.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-menu__overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-menu.open + .hamburger-menu__overlay {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-menu__link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: var(--text-base);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-menu__link:active {
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-menu__link .menu-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Правила:**
|
||||||
|
- 5-8 пунктов максимум
|
||||||
|
- Иконка + текст
|
||||||
|
- Разделители между секциями
|
||||||
|
- Закрывать по тапу на overlay или свайпу
|
||||||
|
- Всегда видный close button
|
||||||
|
|
||||||
|
### 1.4 Bottom Sheet Navigation
|
||||||
|
|
||||||
|
```css
|
||||||
|
.bottom-sheet-nav {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 16px;
|
||||||
|
padding-bottom: calc(16px + var(--safe-bottom));
|
||||||
|
max-height: 70dvh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-sheet-nav.open {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-sheet-nav__handle {
|
||||||
|
width: 40px;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--border-1);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-sheet-nav__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-sheet-nav__item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-sheet-nav__item:active {
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-sheet-nav__item .nav-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.5 Pill Navigation (Chip Scroller)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.pill-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 12px 16px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-nav::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-nav__item {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
border: 1px solid var(--border-1);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
min-height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-nav__item.active {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-nav__item:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Где использовать:** Категории, фильтры, быстрые действия.
|
||||||
|
|
||||||
|
## 2. Navigation Comparison Matrix
|
||||||
|
|
||||||
|
| Паттерн | Кол-во пунктов | Видимость | Доступность | Скорость |
|
||||||
|
|---------|---------------|-----------|-------------|----------|
|
||||||
|
| Bottom Tab | 3-5 | ★★★★★ | ★★★★★ | ★★★★★ |
|
||||||
|
| Top Nav | 2-3 | ★★★★☆ | ★★★★☆ | ★★★★☆ |
|
||||||
|
| Hamburger | 5-8 | ★☆☆☆☆ | ★★★☆☆ | ★★☆☆☆ |
|
||||||
|
| Bottom Sheet | 6-12 | ★☆☆☆☆ | ★★★☆☆ | ★★★☆☆ |
|
||||||
|
| Pill Nav | 5-15 | ★★★★☆ | ★★★★☆ | ★★★★☆ |
|
||||||
|
|
||||||
|
## 3. Navigation Rules
|
||||||
|
|
||||||
|
### Golden Rules
|
||||||
|
|
||||||
|
1. **3-5 основных разделов** — больше требует hierarchy
|
||||||
|
2. **Bottom Tab > Hamburger** — видимая навигация всегда лучше
|
||||||
|
3. **Иконка + текст** — всегда, никогда только иконки
|
||||||
|
4. **Текущий раздел всегда выделен** — не заставляйте пользователя думать
|
||||||
|
5. **Навигация не исчезает при скролле** — sticky или fixed
|
||||||
|
6. **Один паттерн на сайт** — не смешивайте bottom tab и hamburger
|
||||||
|
|
||||||
|
### When to Use What
|
||||||
|
|
||||||
|
| Ситуация | Паттерн |
|
||||||
|
|----------|---------|
|
||||||
|
| 3-5 основных разделов | Bottom Tab Bar |
|
||||||
|
| Много разделов (>5) | Bottom Tab + More (bottom sheet) |
|
||||||
|
| Product page (1 раздел) | Top Nav |
|
||||||
|
| Landing page (1 страница) | Нет навигации или якоря |
|
||||||
|
| Admin panel | Side drawer (left) |
|
||||||
|
| E-commerce categories | Pill Nav |
|
||||||
|
|
||||||
|
## 4. Navigation Patterns for Content
|
||||||
|
|
||||||
|
### 4.1 Swipe to Navigate
|
||||||
|
|
||||||
|
```css
|
||||||
|
.swipe-container {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-container .page {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Breadcrumbs (Mobile)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.breadcrumbs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__item {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__item.active {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__separator {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Tab Content Navigation
|
||||||
|
|
||||||
|
```css
|
||||||
|
.tab-content {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content__header {
|
||||||
|
position: sticky;
|
||||||
|
top: 56px;
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-bottom: 1px solid var(--border-1);
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content__body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content__section {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content__section-title {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
position: sticky;
|
||||||
|
top: 100px;
|
||||||
|
background: var(--surface-1);
|
||||||
|
padding: 8px 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Navigation Anti-Patterns
|
||||||
|
|
||||||
|
| ❌ Плохо | ✅ Хорошо |
|
||||||
|
|----------|-----------|
|
||||||
|
| Hamburger для 3-4 разделов | Bottom Tab Bar |
|
||||||
|
| Скрытие навигации при скролле | Sticky/Fixed навигация |
|
||||||
|
| Только иконки без текста | Иконка + подпись |
|
||||||
|
| 6+ пунктов в bottom nav | Разделить на разделы |
|
||||||
|
| Навигация на top для основного | Bottom для основных |
|
||||||
|
| Нет выделенного активного состояния | Четкий active state |
|
||||||
@@ -0,0 +1,663 @@
|
|||||||
|
---
|
||||||
|
title: Mobile Layout & Pattern Systems
|
||||||
|
created: 2026-07-03
|
||||||
|
updated: 2026-07-03
|
||||||
|
type: system
|
||||||
|
tags: [mobile, layout, patterns, responsive, touch, mobile-first]
|
||||||
|
sources:
|
||||||
|
- https://www.uxpin.com/studio/blog/mobile-navigation-examples/
|
||||||
|
- https://www.uxpin.com/studio/blog/mobile-navigation-patterns-pros-and-cons/
|
||||||
|
- https://www.letsgroto.com/blog/mobile-first-responsive-design-best-practices
|
||||||
|
- https://www.uxpin.com/studio/blog/best-practices-examples-of-excellent-responsive-design/
|
||||||
|
- https://www.startdesigns.com/blog/responsive-web-design-2026/
|
||||||
|
- https://www.jamesrossjr.com/blog/responsive-web-design-best-practices
|
||||||
|
- https://dev.to/gerryleonugroho/responsive-design-breakpoints-2025-playbook-53ih
|
||||||
|
- https://scrimba.com/articles/responsive-web-design-a-complete-guide-2026-2/
|
||||||
|
confidence: high
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mobile Layout & Pattern Systems — Мобильные паттерны верстки
|
||||||
|
|
||||||
|
## 1. Базовые принципы
|
||||||
|
|
||||||
|
### Mobile-First: философия, а не техника
|
||||||
|
|
||||||
|
Мобильный дизайн — это дисциплина приоритизации. На экране 360×800px каждое пиксельное решение имеет вес.
|
||||||
|
|
||||||
|
| Принцип | Описание |
|
||||||
|
|---------|----------|
|
||||||
|
| **Progressive Enhancement** | Базовый контент → улучшенные компоненты → desktop-роскошь |
|
||||||
|
| **One-thumb Zone** | Основные интерактивные элементы в нижней половине экрана |
|
||||||
|
| **Content-First Flow** | Сначала контент, потом декор (всегда) |
|
||||||
|
| **Touch-Optimized** | 44×44px минимальный tap target, 8px gap между touch-элементами |
|
||||||
|
| **Performance-Aware** | Каждый миллисекунда LCP на мобильном важнее десктопа |
|
||||||
|
|
||||||
|
### Глобальные CSS Variables для Mobile
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* Safe Area — iOS notch, Android gesture bar */
|
||||||
|
--safe-top: env(safe-area-inset-top, 20px);
|
||||||
|
--safe-bottom: env(safe-area-inset-bottom, 34px);
|
||||||
|
--safe-left: env(safe-area-inset-left, 0px);
|
||||||
|
--safe-right: env(safe-area-inset-right, 0px);
|
||||||
|
|
||||||
|
/* Typography — fluid on mobile */
|
||||||
|
--text-xs: clamp(0.75rem, 2vw, 0.875rem);
|
||||||
|
--text-sm: clamp(0.875rem, 2.5vw, 1rem);
|
||||||
|
--text-base: clamp(1rem, 3vw, 1.125rem);
|
||||||
|
--text-lg: clamp(1.125rem, 3.5vw, 1.25rem);
|
||||||
|
--text-xl: clamp(1.25rem, 4vw, 1.5rem);
|
||||||
|
|
||||||
|
/* Spacing — mobile-optimized */
|
||||||
|
--space-xs: 0.25rem;
|
||||||
|
--space-sm: 0.5rem;
|
||||||
|
--space-md: 1rem;
|
||||||
|
--space-lg: 1.5rem;
|
||||||
|
--space-xl: 2rem;
|
||||||
|
--space-2xl: 3rem;
|
||||||
|
|
||||||
|
/* Touch targets */
|
||||||
|
--touch-min: 44px;
|
||||||
|
--touch-gap: 8px;
|
||||||
|
|
||||||
|
/* Breakpoints — content-based, not device-based */
|
||||||
|
--bp-sm: 480px; /* Large phones */
|
||||||
|
--bp-md: 768px; /* Tablets portrait */
|
||||||
|
--bp-lg: 1024px; /* Tablets landscape */
|
||||||
|
--bp-xl: 1280px; /* Desktops */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Breakpoints: Content-Based, Not Device-Based
|
||||||
|
|
||||||
|
### Современная система breakpoints
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Content-first breakpoints */
|
||||||
|
@media (min-width: 640px) { /* sm: Large phones, small tablets */ }
|
||||||
|
@media (min-width: 768px) { /* md: Tablets */ }
|
||||||
|
@media (min-width: 1024px) { /* lg: Small laptops */ }
|
||||||
|
@media (min-width: 1280px) { /* xl: Desktops */ }
|
||||||
|
@media (min-width: 1536px) { /* 2xl: Large desktops */ }
|
||||||
|
|
||||||
|
/* Container Queries — component-level адаптивность */
|
||||||
|
.card-container {
|
||||||
|
container-type: inline-size;
|
||||||
|
container-name: card;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container card (min-width: 400px) {
|
||||||
|
.card {
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container card (min-width: 600px) {
|
||||||
|
.card {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Почему container queries > media queries
|
||||||
|
|
||||||
|
| Media Queries | Container Queries |
|
||||||
|
|---------------|-------------------|
|
||||||
|
| Адаптация по viewport | Адаптация по родительскому контейнеру |
|
||||||
|
| Один layout для всей страницы | Разные layout для разных компонентов |
|
||||||
|
| Медленные перерасчеты | Реакция на изменение контейнера |
|
||||||
|
| Не работает в iframe/grid | Работает везде |
|
||||||
|
|
||||||
|
## 3. Mobile Layout Patterns
|
||||||
|
|
||||||
|
### 3.1 Single-Column Stack (Базовый)
|
||||||
|
|
||||||
|
Все элементы вертикально, 16-24px gap между секциями.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.mobile-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
padding: var(--space-lg) var(--space-md) calc(var(--safe-bottom) + var(--space-lg)) var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-stack > * {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Где использовать:** Лендинги, product pages, статьи, landing pages для мобайла.
|
||||||
|
|
||||||
|
### 3.2 Full-Bleed Sections
|
||||||
|
|
||||||
|
Секции, выходящие за границы контейнера, создают глубину.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.full-bleed-section {
|
||||||
|
margin-left: calc(var(--space-md) * -1);
|
||||||
|
margin-right: calc(var(--space-md) * -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-bleed-section .container {
|
||||||
|
padding-left: var(--space-lg);
|
||||||
|
padding-right: var(--space-lg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Где использовать:** Hero, testimonials, image-heavy sections.
|
||||||
|
|
||||||
|
### 3.3 Bento Grid (Mobile Adaptation)
|
||||||
|
|
||||||
|
На мобайле bento grid превращается в single-column с карточками разной высоты.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.bento-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-card {
|
||||||
|
min-height: var(--touch-min);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
border-radius: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-card.featured {
|
||||||
|
order: -1; /* featured card first */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Sticky Bottom CTA
|
||||||
|
|
||||||
|
Кнопка действия всегда видна внизу экрана, над клавиатурой.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.sticky-bottom-cta {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: var(--space-md) var(--space-lg) calc(var(--safe-bottom) + var(--space-md));
|
||||||
|
background: linear-gradient(to top, rgba(0,0,0,0.9), transparent);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-bottom-cta button {
|
||||||
|
min-height: var(--touch-min);
|
||||||
|
width: 100%;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Где использовать:** E-commerce, формы, booking-системы.
|
||||||
|
|
||||||
|
### 3.5 Card-First Layout
|
||||||
|
|
||||||
|
Контент карточками — основной паттерн мобайла.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.card-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-md);
|
||||||
|
padding: var(--space-md) var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-radius: var(--space-md);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
/* Touch-friendly */
|
||||||
|
min-height: var(--touch-min);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__media {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: var(--space-sm) var(--space-sm) 0 0;
|
||||||
|
margin: calc(var(--space-lg) * -1) calc(var(--space-lg) * -1) var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__title {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__meta {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 Masonry (Mobile)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.masonry {
|
||||||
|
columns: 2;
|
||||||
|
column-gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.masonry-item {
|
||||||
|
break-inside: avoid;
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
border-radius: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Single column on very small screens */
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.masonry { columns: 1; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.7 Swipeable Sections
|
||||||
|
|
||||||
|
CSS scroll-snap для горизонтальных секций.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.swipe-section {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-md);
|
||||||
|
overflow-x: auto;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding: var(--space-md) var(--space-lg);
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-section::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-item {
|
||||||
|
scroll-snap-align: start;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: min(280px, 80vw);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Где использовать:** Карусели, product carousels, category pills, testimonial sliders.
|
||||||
|
|
||||||
|
## 4. Mobile Typography
|
||||||
|
|
||||||
|
### Fluid Typography Formula
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Base size scales proportionally */
|
||||||
|
--text-base-size: clamp(1rem, 0.875rem + 0.5vw, 1.25rem);
|
||||||
|
|
||||||
|
/* Scale multiplier */
|
||||||
|
--scale-ratio: 1.25; /* Major Third */
|
||||||
|
--text-sm: calc(var(--text-base-size) / var(--scale-ratio));
|
||||||
|
--text-lg: calc(var(--text-base-size) * var(--scale-ratio));
|
||||||
|
--text-xl: calc(var(--text-lg) * var(--scale-ratio));
|
||||||
|
--text-2xl: calc(var(--text-xl) * var(--scale-ratio));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Line Height & Readability
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Body text — line-height для мобильных */
|
||||||
|
.text-body {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
line-height: 1.6; /* 1.5 на десктопе, 1.6 на мобайле */
|
||||||
|
max-width: 65ch; /* Оптимальная ширина строки */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Headings — tighter on mobile */
|
||||||
|
.text-heading {
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Touch-Optimized Components
|
||||||
|
|
||||||
|
### Touch-First Button
|
||||||
|
|
||||||
|
```css
|
||||||
|
.btn {
|
||||||
|
min-height: var(--touch-min);
|
||||||
|
min-width: var(--touch-min);
|
||||||
|
padding: var(--space-md) var(--space-xl);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--space-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.1s ease, background 0.2s ease;
|
||||||
|
/* Touch feedback */
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Touch-Optimized Form Input
|
||||||
|
|
||||||
|
```css
|
||||||
|
.form-input {
|
||||||
|
min-height: var(--touch-min);
|
||||||
|
padding: var(--space-md) var(--space-lg);
|
||||||
|
font-size: var(--text-base); /* Prevents iOS zoom on focus */
|
||||||
|
border: 2px solid var(--border-1);
|
||||||
|
border-radius: var(--space-md);
|
||||||
|
background: var(--surface-1);
|
||||||
|
transition: border-color 0.15s ease;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* iOS zoom prevention */
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.form-input {
|
||||||
|
font-size: 16px; /* Minimum to prevent zoom on iOS */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Mobile-First CSS Architecture
|
||||||
|
|
||||||
|
### Base → Component → Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
styles/
|
||||||
|
base/
|
||||||
|
_reset.css /* Normalize, box-sizing */
|
||||||
|
_typography.css /* Fluid type scale */
|
||||||
|
_colors.css /* CSS Variables, tokens */
|
||||||
|
_accessibility.css /* Focus states, reduced motion */
|
||||||
|
components/
|
||||||
|
_buttons.css /* Touch-optimized buttons */
|
||||||
|
_inputs.css /* Form fields */
|
||||||
|
_cards.css /* Card patterns */
|
||||||
|
_nav.css /* Mobile nav */
|
||||||
|
layout/
|
||||||
|
_stack.css /* Single-column layout */
|
||||||
|
_grid.css /* Responsive grids */
|
||||||
|
_sections.css /* Section patterns */
|
||||||
|
utilities/
|
||||||
|
_spacing.css /* Margin/padding utilities */
|
||||||
|
_display.css /* Flex, grid utilities */
|
||||||
|
_accessibility.css /* sr-only, reduced-motion */
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reduced Motion
|
||||||
|
|
||||||
|
```css
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Performance Patterns for Mobile
|
||||||
|
|
||||||
|
### Image Optimization
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Progressive image loading -->
|
||||||
|
<img
|
||||||
|
src="thumb.webp"
|
||||||
|
srcset="small.webp 400w, medium.webp 800w, large.webp 1200w"
|
||||||
|
sizes="(max-width: 768px) 100vw, 50vw"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
width="800"
|
||||||
|
height="600"
|
||||||
|
alt="Description"
|
||||||
|
class="lazy-image"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.lazy-image {
|
||||||
|
background: color-mix(in srgb, var(--surface-2) 50%, transparent);
|
||||||
|
animation: image-reveal 0.4s ease forwards;
|
||||||
|
opacity: 0;
|
||||||
|
aspect-ratio: attr(width) / attr(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes image-reveal {
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Critical CSS for Mobile
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Inlined in <head> for mobile LCP optimization */
|
||||||
|
/* Critical path CSS — < 14KB gzipped */
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
min-height: 60vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-xl) var(--space-lg);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lazy Loading with Intersection Observer
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Lightweight lazy loader (no dependencies)
|
||||||
|
const lazyObserver = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const el = entry.target;
|
||||||
|
if (el.dataset.src) {
|
||||||
|
el.src = el.dataset.src;
|
||||||
|
}
|
||||||
|
if (el.dataset.bg) {
|
||||||
|
el.style.backgroundImage = `url(${el.dataset.bg})`;
|
||||||
|
}
|
||||||
|
lazyObserver.unobserve(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
rootMargin: '200px 0px', // Start loading 200px before visible
|
||||||
|
threshold: 0.01
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.lazy').forEach(el => {
|
||||||
|
lazyObserver.observe(el);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. iOS & Android Specific Patterns
|
||||||
|
|
||||||
|
### Viewport Meta Tag
|
||||||
|
|
||||||
|
```html
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
|
```
|
||||||
|
|
||||||
|
### iOS-Specific Fixes
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Prevent iOS Safari bounce effect */
|
||||||
|
body {
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full viewport height fix for iOS */
|
||||||
|
.mobile-container {
|
||||||
|
min-height: 100dvh; /* Dynamic viewport height */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent pull-to-refresh on specific elements */
|
||||||
|
.prevent-pull-to-refresh {
|
||||||
|
overscroll-behavior-y: none;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dark Mode (iOS & Android)
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* System dark mode — auto-applied */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
--text-secondary: rgba(255,255,255,0.7);
|
||||||
|
--surface-1: #1c1c1e;
|
||||||
|
--surface-2: #2c2c2e;
|
||||||
|
--border-1: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force dark mode toggle */
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
--surface-1: #1c1c1e;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Mobile Testing Checklist
|
||||||
|
|
||||||
|
| Category | Check | Tool |
|
||||||
|
|----------|-------|------|
|
||||||
|
| **Layout** | Single-column flow at 320px width | Chrome DevTools |
|
||||||
|
| **Touch** | All tap targets ≥ 44×44px | Lighthouse |
|
||||||
|
| **Typography** | No zoom on iOS input focus | Real device test |
|
||||||
|
| **Performance** | LCP < 2.5s, INP < 200ms, CLS < 0.1 | PageSpeed Insights |
|
||||||
|
| **Safe Area** | Content doesn't overlap notch/home indicator | iPhone real device |
|
||||||
|
| **Scroll** | Horizontal scrolls fixed, vertical smooth | Real device test |
|
||||||
|
| **Forms** | Correct keyboard types for each input | Lighthouse |
|
||||||
|
| **Dark Mode** | All components readable in dark | Developer tools |
|
||||||
|
| **Orientation** | Portrait & landscape tested | Real device test |
|
||||||
|
| **Accessibility** | Touch targets, contrast, labels | axe DevTools |
|
||||||
|
|
||||||
|
## 10. Mobile-First Development Workflow
|
||||||
|
|
||||||
|
### Step 1: Wireframe at 360px
|
||||||
|
|
||||||
|
Начинай всегда с самого маленького экрана. Если не помещается — режь контент.
|
||||||
|
|
||||||
|
### Step 2: Base Styles Mobile-First
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Mobile first — base styles for 320px+ */
|
||||||
|
.component {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-md);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet landscape & up */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.component {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--space-xl);
|
||||||
|
padding: var(--space-2xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.component {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Progressive Enhancement
|
||||||
|
|
||||||
|
1. **Level 1 (Mobile base):** Content, navigation, forms
|
||||||
|
2. **Level 2 (Tablet+):** Two-column layouts, side-by-side components
|
||||||
|
3. **Level 3 (Desktop+):** Animations, complex interactions, floating elements
|
||||||
|
4. **Level 4 (Desktop+):** Parallax, 3D transforms, advanced effects
|
||||||
|
|
||||||
|
### Step 4: Test on Real Devices
|
||||||
|
|
||||||
|
Chrome DevTools — хорошо для макета, но не для производительности. Всегда тестируй на реальном устройстве:
|
||||||
|
- iOS Safari (Zoom, pull-to-refresh, safe area)
|
||||||
|
- Android Chrome (Variable refresh rate, different touch behavior)
|
||||||
|
- 3G/4G throttling (реальная скорость загрузки)
|
||||||
|
|
||||||
|
## 11. Mobile Anti-Patterns
|
||||||
|
|
||||||
|
| ❌ Плохо | ✅ Хорошо |
|
||||||
|
|----------|-----------|
|
||||||
|
| Hamburger-меню для всех навигаций | Bottom tab bar для 3-5 основных разделов |
|
||||||
|
| Hover-эффекты | Active/pressed states для touch |
|
||||||
|
| Фиксированные ширины | Fluid typography + container queries |
|
||||||
|
| Полноценные изображения на всех экранах | Responsive images + lazy loading |
|
||||||
|
| Inline JS анимации | CSS transitions + GPU-accelerated transforms |
|
||||||
|
| 100vh для full-height секций | 100dvh (dynamic viewport) |
|
||||||
|
| Скрытые CTA | Sticky bottom CTA на scrollable страницах |
|
||||||
|
| Автоплей видео | Play/pause по тапу, muted по умолчанию |
|
||||||
|
| Popup-формы на мобайле | Inline-формы, progressive disclosure |
|
||||||
|
|
||||||
|
## 12. Mobile Component Library Checklist
|
||||||
|
|
||||||
|
### Базовые компоненты
|
||||||
|
|
||||||
|
- [ ] **Button** — primary, secondary, ghost, icon-only
|
||||||
|
- [ ] **Input** — text, password, email, tel, number, search
|
||||||
|
- [ ] **Select** — native select (мобильные picker'ы)
|
||||||
|
- [ ] **Card** — image card, text card, media card
|
||||||
|
- [ ] **List** — simple, divider, avatar
|
||||||
|
- [ ] **Navigation** — bottom tab, top nav, breadcrumbs
|
||||||
|
- [ ] **Modal** — full-screen on mobile, centered on desktop
|
||||||
|
- [ ] **Toast/Alert** — dismissible, auto-dismiss
|
||||||
|
- [ ] **Tab Bar** — scrollable on overflow
|
||||||
|
- [ ] **Accordion** — expandable sections (FAQ, settings)
|
||||||
|
|
||||||
|
### Mobile-Specific
|
||||||
|
|
||||||
|
- [ ] **Swipeable Carousel** — touch-enabled
|
||||||
|
- [ ] **Pull-to-Refresh** — native iOS/Android feel
|
||||||
|
- [ ] **Bottom Sheet** — slide-up from bottom
|
||||||
|
- [ ] **Floating Action Button** — contextual actions
|
||||||
|
- [ ] **Sticky Footer** — always-visible CTA
|
||||||
|
|
||||||
|
## 13. Source Links
|
||||||
|
|
||||||
|
- [UXPin: Mobile Navigation Examples](https://www.uxpin.com/studio/blog/mobile-navigation-examples/)
|
||||||
|
- [UXPin: Mobile Navigation Patterns Pros/Cons](https://www.uxpin.com/studio/blog/mobile-navigation-patterns-pros-and-cons/)
|
||||||
|
- [James Ross Jr: Responsive Design Best Practices 2026](https://www.jamesrossjr.com/blog/responsive-web-design-best-practices)
|
||||||
|
- [Scrimba: Responsive Web Design 2026](https://scrimba.com/articles/responsive-web-design-a-complete-guide-2026-2/)
|
||||||
|
- [DEV: Responsive Design Breakpoints 2025](https://dev.to/gerryleonugroho/responsive-design-breakpoints-2025-playbook-53ih)
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
---
|
||||||
|
title: Mobile Performance Patterns
|
||||||
|
created: 2026-07-03
|
||||||
|
updated: 2026-07-03
|
||||||
|
type: system
|
||||||
|
tags: [mobile, performance, core-web-vitals, lcp, inp, cls, optimization]
|
||||||
|
sources:
|
||||||
|
- https://developers.google.com/search/docs/appearance/core-web-vitals
|
||||||
|
- https://rankture.com/blog/core-web-vitals-optimization-guide
|
||||||
|
- https://eastondev.com/blog/en/posts/dev/20251124-core-web-vitals-optimization/
|
||||||
|
- https://ubmetrics.com/blog/core-web-vitals-complete-guide-2025
|
||||||
|
- https://www.airankaudit.com/core-web-vitals-guide-2025-lcp-inp-cls-explained-with-fixes/
|
||||||
|
confidence: high
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mobile Performance Patterns — Производительность для мобильных
|
||||||
|
|
||||||
|
## 1. Core Web Vitals: Мобильные Стандарты 2025/2026
|
||||||
|
|
||||||
|
### 1.1 Thresholds
|
||||||
|
|
||||||
|
| Метрика | Good | Needs Improvement | Poor |
|
||||||
|
|---------|------|-------------------|------|
|
||||||
|
| **LCP** | ≤ 2.5s | 2.5s - 4.0s | > 4.0s |
|
||||||
|
| **INP** | ≤ 200ms | 200ms - 500ms | > 500ms |
|
||||||
|
| **CLS** | ≤ 0.1 | 0.1 - 0.25 | > 0.25 |
|
||||||
|
|
||||||
|
### 1.2 Mobile-First Importance
|
||||||
|
|
||||||
|
На мобильных:
|
||||||
|
- LCP **критичнее** из-за медленных сетей (3G/4G)
|
||||||
|
- INP **критичнее** из-за более слабых CPU
|
||||||
|
- CLS **критичнее** из-за smaller viewport и layout shifts
|
||||||
|
|
||||||
|
### 1.3 Measurement Tools
|
||||||
|
|
||||||
|
| Tool | Purpose | Best For |
|
||||||
|
|------|---------|----------|
|
||||||
|
| PageSpeed Insights | Lab + Field Data | Quick audit |
|
||||||
|
| Chrome UX Report | Real user data | Production monitoring |
|
||||||
|
| Web Vitals JS Library | Runtime measurement | Dev instrumentation |
|
||||||
|
| Lighthouse | Lab testing | Development |
|
||||||
|
| DevTools Network Throttling | Simulated 3G | Testing |
|
||||||
|
|
||||||
|
## 2. Largest Contentful Paint (LCP)
|
||||||
|
|
||||||
|
### 2.1 What is LCP?
|
||||||
|
|
||||||
|
Время, когда **крупнейший элемент контента** (hero image, H1, section) становится видимым.
|
||||||
|
|
||||||
|
### 2.2 LCP Optimization Checklist
|
||||||
|
|
||||||
|
```
|
||||||
|
Priority:
|
||||||
|
1. Critical image — preload + next-gen format
|
||||||
|
2. Above-the-fold content — inline critical CSS
|
||||||
|
3. Server response — reduce TTFB
|
||||||
|
4. Render-blocking resources — defer/async
|
||||||
|
5. JS execution — minify + code split
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Critical Image Optimization
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Hero image: preload as resource -->
|
||||||
|
<link rel="preload" as="image" href="/hero-large.webp" fetchpriority="high">
|
||||||
|
|
||||||
|
<!-- Responsive hero with next-gen formats -->
|
||||||
|
<picture>
|
||||||
|
<source srcset="hero-480.webp 480w, hero-800.webp 800w, hero-1200.webp 1200w"
|
||||||
|
media="(min-width: 768px)">
|
||||||
|
<img
|
||||||
|
src="hero-480.webp"
|
||||||
|
loading="eager"
|
||||||
|
fetchpriority="high"
|
||||||
|
decoding="async"
|
||||||
|
width="800"
|
||||||
|
height="600"
|
||||||
|
alt="Hero description"
|
||||||
|
class="lcp-hero"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Prevent layout shift for LCP image */
|
||||||
|
.lcp-hero {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
object-fit: cover;
|
||||||
|
background: color-mix(in srgb, var(--surface-2) 50%, transparent);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 Critical CSS Inlining
|
||||||
|
|
||||||
|
```html
|
||||||
|
<head>
|
||||||
|
<!-- Inline critical CSS (above-the-fold) -->
|
||||||
|
<style>
|
||||||
|
/* Critical path CSS — target < 14KB gzipped */
|
||||||
|
.hero {
|
||||||
|
min-height: 60vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
.hero__title {
|
||||||
|
font-size: clamp(2rem, 8vw, 4rem);
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
/* ... more critical styles ... */
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Defer non-critical CSS -->
|
||||||
|
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||||
|
<noscript><link rel="stylesheet" href="/styles.css"></noscript>
|
||||||
|
</head>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 Reduce TTFB (Time to First Byte)
|
||||||
|
|
||||||
|
```
|
||||||
|
Optimization:
|
||||||
|
1. Use CDN (Cloudflare, Fastly, AWS CloudFront)
|
||||||
|
2. Edge caching (Vercel, Netlify Edge, Cloudflare Workers)
|
||||||
|
3. HTTP/2 or HTTP/3
|
||||||
|
4. Database query optimization
|
||||||
|
5. Server-side rendering (SSR) or static generation
|
||||||
|
6. Connection pooling
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Interaction to Next Paint (INP)
|
||||||
|
|
||||||
|
### 3.1 What is INP?
|
||||||
|
|
||||||
|
Время между **пользовательским взаимодействием** и следующей отрисовкой. Заменяет FID (First Input Delay).
|
||||||
|
|
||||||
|
### 3.2 INP Optimization Strategies
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 1. Use passive event listeners
|
||||||
|
document.addEventListener('scroll', handler, { passive: true });
|
||||||
|
document.addEventListener('touchstart', handler, { passive: true });
|
||||||
|
|
||||||
|
// 2. Debounce expensive operations
|
||||||
|
function debounce(fn, delay = 16) {
|
||||||
|
let ticking = false;
|
||||||
|
return () => {
|
||||||
|
if (!ticking) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
fn();
|
||||||
|
ticking = false;
|
||||||
|
});
|
||||||
|
ticking = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Offload heavy work to Web Workers
|
||||||
|
// worker.js
|
||||||
|
self.onmessage = (e) => {
|
||||||
|
const result = heavyComputation(e.data);
|
||||||
|
self.postMessage(result);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 4. GPU-accelerated animations */
|
||||||
|
.animated-element {
|
||||||
|
will-change: transform, opacity;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5. Avoid layout thrashing */
|
||||||
|
// Bad: forces layout recalculation
|
||||||
|
element.style.width = '100px';
|
||||||
|
console.log(element.offsetWidth);
|
||||||
|
|
||||||
|
// Good: batch reads
|
||||||
|
const width = element.offsetWidth; // read
|
||||||
|
element.style.width = '100px'; // write
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 INP Debugging Checklist
|
||||||
|
|
||||||
|
| Category | Check | Tool |
|
||||||
|
|----------|-------|------|
|
||||||
|
| **Event handlers** | Passive listeners where possible | DevTools |
|
||||||
|
| **Long tasks** | < 50ms per task | DevTools Long Tasks |
|
||||||
|
| **JS execution** | Code splitting, tree shaking | Lighthouse |
|
||||||
|
| **Layout thrashing** | Batch reads/writes | DevTools |
|
||||||
|
| **Main thread** | No blocking scripts | Performance panel |
|
||||||
|
|
||||||
|
## 4. Cumulative Layout Shift (CLS)
|
||||||
|
|
||||||
|
### 4.1 What is CLS?
|
||||||
|
|
||||||
|
Суммарный сдвиг **всех визуальных элементов** за время загрузки страницы.
|
||||||
|
|
||||||
|
### 4.2 CLS Prevention Patterns
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 1. Always set dimensions on images/videos */
|
||||||
|
img, video, iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Reserve space for dynamic content */
|
||||||
|
.ad-container {
|
||||||
|
min-height: 200px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Font loading — prevent FOIT/FOUT */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'MyFont';
|
||||||
|
src: url('/fonts/myfont.woff2') format('woff2');
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4. Sticky ads — fixed position */
|
||||||
|
.ad-sticky {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5. Skeleton loaders */
|
||||||
|
.skeleton {
|
||||||
|
background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 50%, var(--surface-2) 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 CLS Measurement
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Web Vitals library for CLS
|
||||||
|
import { getCLS } from 'web-vitals';
|
||||||
|
|
||||||
|
getCLS((value) => {
|
||||||
|
console.log('CLS:', value);
|
||||||
|
// Report to analytics
|
||||||
|
if (value > 0.25) {
|
||||||
|
sendToAnalytics('cls_threshold_exceeded', { value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Image Optimization
|
||||||
|
|
||||||
|
### 5.1 Image Strategy for Mobile
|
||||||
|
|
||||||
|
```
|
||||||
|
Priority:
|
||||||
|
1. Use next-gen formats (WebP, AVIF)
|
||||||
|
2. Responsive images with srcset
|
||||||
|
3. Lazy loading for below-fold images
|
||||||
|
4. Proper dimensions (width/height)
|
||||||
|
5. Compress images (Squoosh, TinyPNG)
|
||||||
|
6. CDN for image delivery
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Responsive Images Pattern
|
||||||
|
|
||||||
|
```html
|
||||||
|
<picture>
|
||||||
|
<source srcset="image-small.webp 400w, image-medium.webp 800w, image-large.webp 1200w"
|
||||||
|
media="(min-width: 768px)">
|
||||||
|
<source srcset="image-small.avif 400w, image-medium.avif 800w, image-large.avif 1200w"
|
||||||
|
media="(min-width: 768px)">
|
||||||
|
<img
|
||||||
|
src="image-small.webp"
|
||||||
|
srcset="image-small.webp 400w, image-medium.webp 800w, image-large.webp 1200w"
|
||||||
|
sizes="(max-width: 767px) 100vw, 50vw"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
width="800"
|
||||||
|
height="600"
|
||||||
|
alt="Description"
|
||||||
|
class="lazy-image"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Lazy Loading with Intersection Observer
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class LazyLoader {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.threshold = options.threshold || 0.1;
|
||||||
|
this.rootMargin = options.rootMargin || '200px 0px';
|
||||||
|
this.observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const el = entry.target;
|
||||||
|
|
||||||
|
if (el.tagName === 'IMG') {
|
||||||
|
if (el.dataset.src) el.src = el.dataset.src;
|
||||||
|
if (el.dataset.srcset) el.srcset = el.dataset.srcset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.tagName === 'VIDEO') {
|
||||||
|
if (el.dataset.src) el.src = el.dataset.src;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.classList.add('loaded');
|
||||||
|
this.observer.unobserve(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
threshold: this.threshold,
|
||||||
|
rootMargin: this.rootMargin
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
observe(elements) {
|
||||||
|
elements.forEach(el => this.observer.observe(el));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const lazyLoader = new LazyLoader({ rootMargin: '200px 0px' });
|
||||||
|
document.querySelectorAll('.lazy').forEach(el => lazyLoader.observe(el));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. CSS & JS Optimization
|
||||||
|
|
||||||
|
### 6.1 CSS Optimization
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 1. Use CSS containment */
|
||||||
|
.contained {
|
||||||
|
contain: layout style paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Minimize reflows */
|
||||||
|
.element {
|
||||||
|
will-change: transform;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Use CSS variables for theming (avoids JS) */
|
||||||
|
:root {
|
||||||
|
--primary: #0070f3;
|
||||||
|
--text-primary: #111;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 JavaScript Optimization
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 1. Code splitting
|
||||||
|
const lazyModule = import('./heavy-module.js');
|
||||||
|
|
||||||
|
// 2. Debounce user input
|
||||||
|
function debounce(fn, delay = 16) {
|
||||||
|
let ticking = false;
|
||||||
|
return () => {
|
||||||
|
if (!ticking) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
fn();
|
||||||
|
ticking = false;
|
||||||
|
});
|
||||||
|
ticking = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Virtual scroll for long lists
|
||||||
|
function virtualScroll(container, items, itemHeight) {
|
||||||
|
const visibleCount = Math.ceil(container.clientHeight / itemHeight);
|
||||||
|
const scrollTop = container.scrollTop;
|
||||||
|
const startIndex = Math.floor(scrollTop / itemHeight);
|
||||||
|
const endIndex = Math.min(startIndex + visibleCount + 10, items.length);
|
||||||
|
|
||||||
|
// Only render visible items
|
||||||
|
for (let i = startIndex; i < endIndex; i++) {
|
||||||
|
renderItem(items[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Use requestAnimationFrame for animations
|
||||||
|
function animateElement(element, keyframes, options) {
|
||||||
|
element.animate(keyframes, {
|
||||||
|
...options,
|
||||||
|
fill: 'forwards'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Network Optimization
|
||||||
|
|
||||||
|
### 7.1 Service Worker Caching
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// sw.js
|
||||||
|
const CACHE_NAME = 'mobile-app-v1';
|
||||||
|
const ASSETS_TO_CACHE = [
|
||||||
|
'/',
|
||||||
|
'/styles.css',
|
||||||
|
'/app.js',
|
||||||
|
'/fonts/*'
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS_TO_CACHE))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then((response) => {
|
||||||
|
return response || fetch(event.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Prefetch & Preconnect
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Preconnect to critical origins -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="preconnect" href="https://api.example.com">
|
||||||
|
|
||||||
|
<!-- Prefetch critical resources -->
|
||||||
|
<link rel="prefetch" href="/next-page.js">
|
||||||
|
<link rel="prefetch" href="/critical-image.webp">
|
||||||
|
|
||||||
|
<!-- Preload critical resources -->
|
||||||
|
<link rel="preload" href="/hero-large.webp" as="image" fetchpriority="high">
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Mobile Performance Checklist
|
||||||
|
|
||||||
|
| Metric | Target | Tool |
|
||||||
|
|--------|--------|------|
|
||||||
|
| **LCP** | ≤ 2.5s | PageSpeed Insights |
|
||||||
|
| **INP** | ≤ 200ms | Web Vitals JS |
|
||||||
|
| **CLS** | ≤ 0.1 | PageSpeed Insights |
|
||||||
|
| **FCP** | ≤ 1.8s | DevTools |
|
||||||
|
| **TTFB** | ≤ 600ms | Network panel |
|
||||||
|
| **Total JS** | ≤ 200KB | Lighthouse |
|
||||||
|
| **Total CSS** | ≤ 50KB (critical) | Lighthouse |
|
||||||
|
| **Images** | Next-gen + responsive | PageSpeed Insights |
|
||||||
|
|
||||||
|
## 9. Mobile Performance Anti-Patterns
|
||||||
|
|
||||||
|
| ❌ Плохо | ✅ Хорошо |
|
||||||
|
|----------|-----------|
|
||||||
|
| 100vh для полноэкранных секций | 100dvh |
|
||||||
|
| Полноэкранные изображения | Responsive images + lazy loading |
|
||||||
|
| Тяжелые JS-библиотеки | Vanilla JS / lightweight alternatives |
|
||||||
|
| Автоплей видео | Play/pause по тапу |
|
||||||
|
| Шрифты без font-display | font-display: swap |
|
||||||
|
| Без сжатия изображений | WebP/AVIF + compression |
|
||||||
|
| Синхронный JS в <head> | Async/defer |
|
||||||
Reference in new issue
Block a user