Responsive

Модуль 4: Адаптивная вёрстка

Полное погружение в мир responsive design — от базовых медиа-запросов до современных контейнерных запросов и fluid typography.

Введение в адаптивную вёрстку

Что такое адаптивный дизайн?

Responsive Web Design (RWD) — подход к веб-разработке, при котором сайт автоматически подстраивается под размер экрана устройства пользователя.

Три столпа адаптивности

  • Гибкая сетка (Flexible Grid) — размеры в процентах или относительных единицах
  • Гибкие изображения (Flexible Images) — масштабирование медиа-контента
  • Медиа-запросы (Media Queries) — условные стили для разных экранов

Почему это важно?

Статистика Данные 2025
Мобильный трафик ~60% всего веб-трафика
Google Mobile-First Индексация с мобильной версии
Bounce rate без адаптива +50% отказов
Конверсия +30% на адаптивных сайтах

Эволюция адаптивности

2000-е

Фиксированные макеты 960px, отдельная мобильная версия (m.site.com)

2010

Ethan Marcotte вводит термин "Responsive Web Design"

2015

Google объявляет mobile-friendly как фактор ранжирования

2019

Mobile-First Indexing становится стандартом

2023+

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

Настройки

Цветовая схема

Тема