Модуль 4: Адаптивная вёрстка
Полное погружение в мир responsive design — от базовых медиа-запросов до современных контейнерных запросов и fluid typography.
Введение в адаптивную вёрстку
Что такое адаптивный дизайн?
Responsive Web Design (RWD) — подход к веб-разработке, при котором сайт автоматически подстраивается под размер экрана устройства пользователя.
Три столпа адаптивности
- Гибкая сетка (Flexible Grid) — размеры в процентах или относительных единицах
- Гибкие изображения (Flexible Images) — масштабирование медиа-контента
- Медиа-запросы (Media Queries) — условные стили для разных экранов
Почему это важно?
| Статистика | Данные 2025 |
|---|---|
| Мобильный трафик | ~60% всего веб-трафика |
| Google Mobile-First | Индексация с мобильной версии |
| Bounce rate без адаптива | +50% отказов |
| Конверсия | +30% на адаптивных сайтах |
Эволюция адаптивности
Фиксированные макеты 960px, отдельная мобильная версия (m.site.com)
Ethan Marcotte вводит термин "Responsive Web Design"
Google объявляет mobile-friendly как фактор ранжирования
Mobile-First Indexing становится стандартом
Container Queries, :has(), subgrid — новая эра CSS
Viewport и единицы измерения
Meta viewport
Критически важный тег для корректного отображения на мобильных устройствах:
<!-- Базовая настройка (обязательно!) -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Расширенные параметры -->
<meta name="viewport" content="
width=device-width,
initial-scale=1.0,
minimum-scale=1.0,
maximum-scale=5.0,
user-scalable=yes
">
Никогда не делайте так
user-scalable=no или maximum-scale=1.0 — это нарушает доступность! Пользователи с плохим зрением должны иметь возможность увеличить страницу.
Единицы измерения viewport
| Единица | Описание | Пример |
|---|---|---|
vw |
1% ширины viewport | width: 50vw = 50% ширины экрана |
vh |
1% высоты viewport | height: 100vh = полная высота |
vmin |
1% от меньшего измерения | Квадратные элементы |
vmax |
1% от большего измерения | Растянутые элементы |
dvh |
Динамическая высота (учитывает UI) | Мобильные браузеры |
svh |
Минимальная высота viewport | Когда UI развёрнут |
lvh |
Максимальная высота viewport | Когда UI скрыт |
Проблема 100vh на мобильных
На мобильных браузерах 100vh включает адресную строку, что вызывает проблемы:
/* Проблема: контент обрезается под адресной строкой */
.hero {
height: 100vh; /* НЕ рекомендуется */
}
/* Решение 1: Современные единицы (2023+) */
.hero {
height: 100dvh; /* Динамическая высота */
}
/* Решение 2: Fallback для старых браузеров */
.hero {
height: 100vh;
height: 100dvh;
}
/* Решение 3: CSS custom property через JS */
.hero {
height: calc(var(--vh, 1vh) * 100);
}
// JS для вычисления реальной высоты viewport
function setVH() {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
setVH();
window.addEventListener('resize', setVH);
Относительные единицы
/* em — относительно font-size родителя */
.parent { font-size: 16px; }
.child { font-size: 1.5em; } /* = 24px */
.child { padding: 1em; } /* = 24px (от своего font-size!) */
/* rem — относительно font-size :root (html) */
:root { font-size: 16px; }
.element {
font-size: 1.25rem; /* = 20px */
padding: 1.5rem; /* = 24px */
margin: 2rem; /* = 32px */
}
/* ch — ширина символа "0" */
.input { width: 20ch; } /* ~20 символов */
/* ex — высота строчной "x" */
.sup { vertical-align: 1ex; }
Рекомендация
Используйте rem для размеров и отступов, em для внутренних пропорций компонентов. Это упрощает масштабирование всего интерфейса.
Медиа-запросы (Media Queries)
Базовый синтаксис
/* Синтаксис */
@media media-type and (condition) {
/* стили */
}
/* Типы медиа */
@media screen { } /* Экраны */
@media print { } /* Печать */
@media all { } /* Все устройства (по умолчанию) */
/* Условия по ширине */
@media (min-width: 768px) { } /* От 768px и шире */
@media (max-width: 767px) { } /* До 767px */
@media (width: 1024px) { } /* Точно 1024px (редко) */
/* Диапазоны (современный синтаксис) */
@media (width >= 768px) { }
@media (768px <= width <= 1024px) { }
@media (width < 768px) { }
Логические операторы
/* AND — все условия должны выполняться */
@media screen and (min-width: 768px) and (max-width: 1024px) {
/* Планшеты */
}
/* OR (запятая) — хотя бы одно условие */
@media (max-width: 600px), (orientation: portrait) {
/* Мобильные ИЛИ портретная ориентация */
}
/* NOT — инверсия */
@media not print {
/* Всё кроме печати */
}
/* Современный синтаксис с or/and/not */
@media (width >= 768px) and (width <= 1024px) { }
@media (width < 768px) or (orientation: portrait) { }
@media not (color) { /* Чёрно-белые экраны */ }
Медиа-характеристики (Media Features)
| Характеристика | Описание | Пример |
|---|---|---|
orientation |
Ориентация экрана | portrait / landscape |
aspect-ratio |
Соотношение сторон | (aspect-ratio: 16/9) |
resolution |
Плотность пикселей | (min-resolution: 2dppx) |
prefers-color-scheme |
Тёмная/светлая тема | dark / light |
prefers-reduced-motion |
Отключить анимации | reduce |
prefers-contrast |
Повышенный контраст | more / less |
hover |
Поддержка hover | hover / none |
pointer |
Точность указателя | fine / coarse |
Практические примеры
/* Тёмная тема системы */
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a2e;
--text: #e0e0e0;
}
}
/* Отключение анимаций для пользователей с вестибулярными нарушениями */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
/* Touch-устройства (нет точного hover) */
@media (hover: none) and (pointer: coarse) {
.tooltip { display: none; }
.button { min-height: 44px; } /* Минимум для тапа */
}
/* Retina-дисплеи */
@media (min-resolution: 2dppx) {
.logo {
background-image: url('logo@2x.png');
}
}
/* Печать */
@media print {
.no-print { display: none; }
a[href]::after { content: ' (' attr(href) ')'; }
body { font-size: 12pt; color: black; }
}
Практика
Создайте страницу, которая автоматически переключается между светлой и тёмной темой в зависимости от системных настроек пользователя.
Mobile-First подход
Философия Mobile-First
Mobile-First — методология, при которой сначала создаётся мобильная версия, а затем добавляются стили для больших экранов.
Почему Mobile-First?
- Приоритизация контента — на маленьком экране видно только важное
- Производительность — мобильные устройства загружают меньше CSS
- Прогрессивное улучшение — базовые стили работают везде
- SEO — Google индексирует мобильную версию первой
Desktop-First vs Mobile-First
/* Desktop-First (устаревший подход) */
.container {
width: 1200px;
display: flex;
gap: 2rem;
}
@media (max-width: 768px) {
.container {
width: 100%;
flex-direction: column;
gap: 1rem;
}
}
/* Mobile-First (рекомендуемый подход) */
.container {
width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
}
@media (min-width: 768px) {
.container {
max-width: 1200px;
flex-direction: row;
gap: 2rem;
}
}
Структура Mobile-First CSS
/* 1. Базовые стили (мобильные) */
.card {
padding: 1rem;
border-radius: 8px;
background: var(--card-bg);
}
.card__title {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.card__image {
width: 100%;
aspect-ratio: 16/9;
object-fit: cover;
}
/* 2. Планшеты (от 768px) */
@media (min-width: 768px) {
.card {
padding: 1.5rem;
display: flex;
gap: 1.5rem;
}
.card__image {
width: 40%;
aspect-ratio: 4/3;
}
}
/* 3. Десктоп (от 1024px) */
@media (min-width: 1024px) {
.card {
padding: 2rem;
}
.card__title {
font-size: 1.5rem;
}
}
/* 4. Большие экраны (от 1440px) */
@media (min-width: 1440px) {
.card {
max-width: 1200px;
margin: 0 auto;
}
}
Организация медиа-запросов
/* Вариант 1: Медиа-запросы в конце каждого компонента */
.header { /* мобильные стили */ }
.header__nav { /* мобильные стили */ }
@media (min-width: 768px) {
.header { /* планшетные стили */ }
.header__nav { /* планшетные стили */ }
}
/* Вариант 2: Все медиа-запросы в конце файла */
/* (Лучше для читаемости больших файлов) */
/* Вариант 3: CSS Custom Properties для адаптивности */
:root {
--spacing: 1rem;
--font-size-heading: 1.5rem;
}
@media (min-width: 768px) {
:root {
--spacing: 1.5rem;
--font-size-heading: 2rem;
}
}
@media (min-width: 1024px) {
:root {
--spacing: 2rem;
--font-size-heading: 2.5rem;
}
}
.section {
padding: var(--spacing);
}
h1 {
font-size: var(--font-size-heading);
}
Совет
Используйте CSS Custom Properties для создания "адаптивных токенов" — переменных, которые меняются в зависимости от размера экрана. Это делает код чище и проще в поддержке.
Брейкпоинты и стратегии
Популярные брейкпоинты
| Устройство | Брейкпоинт | Tailwind | Bootstrap |
|---|---|---|---|
| Мобильные (портрет) | 320px - 480px | — | — |
| Мобильные (ландшафт) | 481px - 639px | sm: 640px | sm: 576px |
| Планшеты | 640px - 1023px | md: 768px | md: 768px |
| Ноутбуки | 1024px - 1279px | lg: 1024px | lg: 992px |
| Десктопы | 1280px - 1535px | xl: 1280px | xl: 1200px |
| Большие экраны | 1536px+ | 2xl: 1536px | xxl: 1400px |
Определение своих брейкпоинтов
/* CSS Custom Properties для брейкпоинтов */
:root {
/* Используем в JS для медиа-запросов */
--bp-sm: 640px;
--bp-md: 768px;
--bp-lg: 1024px;
--bp-xl: 1280px;
--bp-2xl: 1536px;
}
/* SCSS/Sass миксины */
@mixin sm { @media (min-width: 640px) { @content; } }
@mixin md { @media (min-width: 768px) { @content; } }
@mixin lg { @media (min-width: 1024px) { @content; } }
@mixin xl { @media (min-width: 1280px) { @content; } }
/* Использование */
.element {
font-size: 1rem;
@include md {
font-size: 1.25rem;
}
@include lg {
font-size: 1.5rem;
}
}
Content-First брейкпоинты
Лучшая практика — определять брейкпоинты на основе контента, а не устройств:
/* Брейкпоинты на основе устройств */
@media (min-width: 768px) { /* "для iPad" */ }
/* Брейкпоинты на основе контента */
/* Открываем DevTools, уменьшаем ширину,
добавляем брейкпоинт когда дизайн "ломается" */
.article {
/* Базовые стили */
}
/* Когда текст становится слишком широким для чтения */
@media (min-width: 45em) {
.article {
max-width: 65ch; /* ~65 символов — идеальная длина строки */
margin: 0 auto;
}
}
/* Когда карточки могут встать в ряд */
@media (min-width: 600px) {
.cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
}
/* Когда помещается третья колонка */
@media (min-width: 900px) {
.cards {
grid-template-columns: repeat(3, 1fr);
}
}
Правило
Добавляйте брейкпоинт только когда дизайн "требует" этого — когда контент выглядит плохо или неудобен для использования.
Fluid Typography (Плавная типографика)
Проблема фиксированных размеров
При использовании медиа-запросов размер шрифта меняется "скачками". Fluid typography обеспечивает плавное изменение.
CSS clamp() — современное решение
/* Синтаксис: clamp(минимум, предпочтительное, максимум) */
h1 {
/* Минимум 2rem, максимум 4rem,
между ними масштабируется от ширины viewport */
font-size: clamp(2rem, 5vw + 1rem, 4rem);
}
p {
/* 1rem на мобильных, плавно до 1.25rem на десктопе */
font-size: clamp(1rem, 0.5vw + 0.875rem, 1.25rem);
}
/* Система fluid typography */
:root {
--fluid-min-width: 320;
--fluid-max-width: 1440;
--fluid-screen: 100vw;
--fluid-bp: calc(
(var(--fluid-screen) - var(--fluid-min-width) / 16 * 1rem) /
(var(--fluid-max-width) - var(--fluid-min-width))
);
}
/* Fluid размеры */
:root {
--fs-sm: clamp(0.875rem, 0.8rem + 0.25vw, 1rem);
--fs-base: clamp(1rem, 0.9rem + 0.35vw, 1.125rem);
--fs-md: clamp(1.125rem, 1rem + 0.5vw, 1.375rem);
--fs-lg: clamp(1.5rem, 1.25rem + 1vw, 2rem);
--fs-xl: clamp(2rem, 1.5rem + 2vw, 3rem);
--fs-2xl: clamp(2.5rem, 1.75rem + 3vw, 4.5rem);
}
Калькулятор Fluid Typography
Формула для расчёта: clamp(minSize, calc(minSize + (maxSize - minSize) * ((100vw - minViewport) / (maxViewport - minViewport))), maxSize)
/* Упрощённая формула */
/* font-size от 16px (320px) до 24px (1440px) */
font-size: clamp(1rem, calc(1rem + (1.5 - 1) * ((100vw - 320px) / (1440 - 320))), 1.5rem);
/* Ещё проще с vw */
/* ~16px на 320px, ~24px на 1440px */
font-size: clamp(1rem, 0.714vw + 0.857rem, 1.5rem);
Fluid Spacing
:root {
/* Fluid отступы */
--space-3xs: clamp(0.25rem, 0.2rem + 0.2vw, 0.375rem);
--space-2xs: clamp(0.5rem, 0.4rem + 0.4vw, 0.75rem);
--space-xs: clamp(0.75rem, 0.6rem + 0.6vw, 1rem);
--space-sm: clamp(1rem, 0.8rem + 0.8vw, 1.5rem);
--space-md: clamp(1.5rem, 1.2rem + 1.2vw, 2rem);
--space-lg: clamp(2rem, 1.6rem + 1.6vw, 3rem);
--space-xl: clamp(3rem, 2.4rem + 2.4vw, 4.5rem);
--space-2xl: clamp(4rem, 3.2rem + 3.2vw, 6rem);
}
.section {
padding: var(--space-lg) var(--space-md);
}
.card {
padding: var(--space-sm);
gap: var(--space-xs);
}
Практика
Создайте систему fluid typography с 5 размерами шрифта, которые плавно масштабируются от 320px до 1440px.
Адаптивные изображения
Проблема
Одно изображение 2000px весит много и медленно грузится на мобильных. Разные экраны требуют разных размеров и форматов.
srcset и sizes
<!-- Базовый srcset с указанием ширины -->
<img
src="image-800.jpg"
srcset="
image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w,
image-1600.jpg 1600w
"
sizes="
(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw
"
alt="Описание изображения"
>
<!-- Объяснение sizes:
- До 600px: изображение занимает 100% ширины viewport
- До 1200px: изображение занимает 50% ширины
- Иначе: 33% ширины
Браузер выбирает оптимальный размер из srcset
-->
picture для art direction
<!-- Разные изображения для разных экранов -->
<picture>
<!-- Мобильные: вертикальный кроп -->
<source
media="(max-width: 600px)"
srcset="hero-mobile.webp"
>
<!-- Планшеты: квадратный кроп -->
<source
media="(max-width: 1024px)"
srcset="hero-tablet.webp"
>
<!-- Десктоп: горизонтальный -->
<img src="hero-desktop.jpg" alt="Hero">
</picture>
<!-- Современные форматы с fallback -->
<picture>
<source type="image/avif" srcset="image.avif">
<source type="image/webp" srcset="image.webp">
<img src="image.jpg" alt="Описание">
</picture>
CSS для адаптивных изображений
/* Базовые правила */
img {
max-width: 100%;
height: auto;
display: block;
}
/* Сохранение пропорций */
.image-container {
aspect-ratio: 16 / 9;
overflow: hidden;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
/* Разные пропорции для разных экранов */
.hero-image {
aspect-ratio: 1 / 1; /* Квадрат на мобильных */
}
@media (min-width: 768px) {
.hero-image {
aspect-ratio: 16 / 9;
}
}
@media (min-width: 1024px) {
.hero-image {
aspect-ratio: 21 / 9; /* Ультраширокий */
}
}
/* Ретина-изображения через CSS */
.logo {
background-image: url('logo.png');
background-size: contain;
}
@media (min-resolution: 2dppx) {
.logo {
background-image: url('logo@2x.png');
}
}
Ленивая загрузка
<!-- Нативная ленивая загрузка -->
<img
src="image.jpg"
loading="lazy"
decoding="async"
alt="Описание"
>
<!-- Важные изображения (hero, above the fold) -->
<img
src="hero.jpg"
loading="eager"
fetchpriority="high"
alt="Hero"
>
<!-- Preload для критичных изображений -->
<link
rel="preload"
as="image"
href="hero.webp"
type="image/webp"
>
Оптимизация изображений
- Используйте современные форматы: AVIF > WebP > JPEG
- Сжимайте изображения (TinyPNG, Squoosh)
- Указывайте width и height для предотвращения CLS
- Используйте CDN с автоматической оптимизацией
Container Queries (Контейнерные запросы)
Революция в CSS
Container Queries позволяют стилизовать элементы на основе размера их контейнера, а не viewport. Это меняет всё!
Базовый синтаксис
/* 1. Определяем контейнер */
.card-container {
container-type: inline-size;
container-name: card;
}
/* Или сокращённо */
.card-container {
container: card / inline-size;
}
/* 2. Используем контейнерные запросы */
@container card (min-width: 400px) {
.card {
display: flex;
gap: 1rem;
}
.card__image {
width: 40%;
}
}
@container card (min-width: 600px) {
.card__title {
font-size: 1.5rem;
}
}
Типы контейнеров
| Тип | Описание |
|---|---|
inline-size |
Запросы по ширине (наиболее частый) |
size |
Запросы по ширине и высоте |
normal |
Не является контейнером для size queries |
Единицы контейнера
.card-container {
container-type: inline-size;
}
.card__title {
/* cqw — 1% ширины контейнера */
font-size: clamp(1rem, 5cqw, 2rem);
/* cqh — 1% высоты контейнера */
/* cqi — 1% inline размера */
/* cqb — 1% block размера */
/* cqmin — меньшее из cqi/cqb */
/* cqmax — большее из cqi/cqb */
}
Практический пример: адаптивная карточка
/* Контейнер */
.card-wrapper {
container: card / inline-size;
}
/* Базовые стили карточки (мобильные) */
.card {
display: grid;
gap: 1rem;
padding: 1rem;
background: var(--card-bg);
border-radius: 12px;
}
.card__image {
aspect-ratio: 16/9;
border-radius: 8px;
overflow: hidden;
}
.card__image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.card__content {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.card__title {
font-size: 1.125rem;
font-weight: 600;
}
/* Когда контейнер >= 350px: горизонтальный layout */
@container card (min-width: 350px) {
.card {
grid-template-columns: 120px 1fr;
align-items: start;
}
.card__image {
aspect-ratio: 1;
}
}
/* Когда контейнер >= 500px: больше места */
@container card (min-width: 500px) {
.card {
grid-template-columns: 180px 1fr;
padding: 1.5rem;
gap: 1.5rem;
}
.card__title {
font-size: 1.375rem;
}
}
/* Когда контейнер >= 700px: еще больше */
@container card (min-width: 700px) {
.card {
grid-template-columns: 250px 1fr;
}
.card__image {
aspect-ratio: 4/3;
}
}
Container Queries vs Media Queries
Когда использовать
- Media Queries — общий layout страницы, навигация, количество колонок
- Container Queries — компоненты, которые могут быть в разных местах (карточки, виджеты)
/* Комбинация Media + Container Queries */
/* Media Query для общего layout */
@media (min-width: 768px) {
.sidebar {
width: 300px;
}
.main {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
}
/* Container Query для компонента */
.card-wrapper {
container: card / inline-size;
}
@container card (min-width: 400px) {
.card {
flex-direction: row;
}
}
Практика
Создайте компонент карточки товара, который адаптируется с помощью Container Queries: вертикальный на узком контейнере, горизонтальный на широком.
Паттерны адаптивной вёрстки
1. Mostly Fluid
Контент растягивается до максимальной ширины, затем центрируется.
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
@media (min-width: 768px) {
.container {
padding: 0 2rem;
}
}
2. Column Drop
Колонки "падают" друг под друга при уменьшении ширины.
.columns {
display: flex;
flex-wrap: wrap;
}
.column {
flex: 1 1 100%;
}
@media (min-width: 600px) {
.column {
flex: 1 1 50%;
}
}
@media (min-width: 900px) {
.column {
flex: 1 1 33.33%;
}
}
3. Layout Shifter
Разные layouts на разных размерах (с CSS Grid).
.layout {
display: grid;
gap: 1rem;
}
/* Мобильные: всё в одну колонку */
.layout {
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
}
/* Планшеты: sidebar справа */
@media (min-width: 768px) {
.layout {
grid-template-columns: 1fr 300px;
grid-template-areas:
"header header"
"main sidebar"
"footer footer";
}
}
/* Десктоп: sidebar слева */
@media (min-width: 1024px) {
.layout {
grid-template-columns: 250px 1fr 300px;
grid-template-areas:
"header header header"
"nav main sidebar"
"footer footer footer";
}
}
4. Off-Canvas
Контент (обычно навигация) скрывается за пределами экрана.
.nav {
position: fixed;
top: 0;
left: 0;
width: 280px;
height: 100vh;
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 1000;
}
.nav.is-open {
transform: translateX(0);
}
@media (min-width: 1024px) {
.nav {
position: static;
transform: none;
width: auto;
height: auto;
}
}
5. Responsive Tables
/* Вариант 1: Горизонтальный скролл */
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Вариант 2: Карточки на мобильных */
@media (max-width: 600px) {
table, thead, tbody, th, td, tr {
display: block;
}
thead {
display: none;
}
tr {
margin-bottom: 1rem;
border: 1px solid var(--border);
border-radius: 8px;
}
td {
display: flex;
justify-content: space-between;
padding: 0.5rem 1rem;
border-bottom: 1px solid var(--border);
}
td::before {
content: attr(data-label);
font-weight: 600;
}
}
<!-- HTML для responsive table -->
<table>
<thead>
<tr>
<th>Имя</th>
<th>Email</th>
<th>Роль</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Имя">Иван</td>
<td data-label="Email">ivan@mail.ru</td>
<td data-label="Роль">Админ</td>
</tr>
</tbody>
</table>
Тестирование адаптивности
Инструменты браузера
- Chrome DevTools — Device Mode (Ctrl+Shift+M), throttling
- Firefox — Responsive Design Mode
- Safari — Responsive Design Mode
Онлайн-инструменты
- Responsively — просмотр на нескольких устройствах
- BrowserStack — реальные устройства
- LambdaTest — кросс-браузерное тестирование
Автоматизированное тестирование
// Playwright — тестирование на разных viewport
import { test, expect } from '@playwright/test';
const viewports = [
{ width: 375, height: 667, name: 'iPhone SE' },
{ width: 768, height: 1024, name: 'iPad' },
{ width: 1440, height: 900, name: 'Desktop' },
];
for (const viewport of viewports) {
test(`Homepage on ${viewport.name}`, async ({ page }) => {
await page.setViewportSize({
width: viewport.width,
height: viewport.height
});
await page.goto('/');
// Скриншот для визуального сравнения
await expect(page).toHaveScreenshot(`home-${viewport.name}.png`);
// Проверка видимости элементов
if (viewport.width < 768) {
await expect(page.locator('.mobile-menu-btn')).toBeVisible();
await expect(page.locator('.desktop-nav')).toBeHidden();
} else {
await expect(page.locator('.mobile-menu-btn')).toBeHidden();
await expect(page.locator('.desktop-nav')).toBeVisible();
}
});
}
Чек-лист тестирования
- Текст читаем на всех размерах
- Кнопки достаточно большие для тапа (минимум 44x44px)
- Изображения не обрезаются и не искажаются
- Формы удобны для заполнения
- Навигация доступна
- Нет горизонтального скролла
- Контент не выходит за пределы экрана
- Модальные окна работают корректно
- Тёмная тема работает
- Производительность приемлема
Best Practices
Общие принципы
- Mobile-First — всегда начинайте с мобильной версии
- Content-First — брейкпоинты на основе контента, не устройств
- Fluid > Fixed — предпочитайте относительные единицы
- Тестируйте на реальных устройствах — эмуляторы не идеальны
Производительность
- Используйте
srcsetиsizesдля изображений - Ленивая загрузка для контента ниже fold
- Минимизируйте количество медиа-запросов
- Используйте CSS Custom Properties для адаптивных токенов
Доступность
- Не отключайте zoom (
user-scalable=yes) - Достаточный размер touch targets (44x44px минимум)
- Уважайте
prefers-reduced-motion - Контраст должен быть достаточным на всех экранах
Чек-лист перед публикацией
<!-- Обязательные meta-теги -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#ffffff">
<!-- Preload критичных ресурсов -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero.webp" as="image">
<!-- Адаптивные изображения -->
<img srcset="..." sizes="..." loading="lazy" alt="...">
<!-- Touch-friendly elements -->
<button style="min-height: 44px; min-width: 44px;">...</button>
Итоговый проект
Создайте полностью адаптивный landing page с использованием:
- Mobile-First подход
- Fluid typography
- Container Queries для карточек
- Адаптивные изображения
- Тёмная тема через
prefers-color-scheme