Все страницы, которые мы строили с Уроков 1 по 11 — включая Flexbox-практику и страницу фильмов на Grid — рассчитаны на десктопный экран. Если открыть их на телефоне, сетка карточек сплющится, aside с фильтрами сожмётся до нечитаемой полоски, а header с логотипом, названием и навигацией в одну строку вообще может вылезти за пределы экрана.
Мы намеренно откладывали эту тему до отдельного урока — в уроках про Flexbox и Grid было сказано «адаптивную вёрстку пока не углубляем». Пришло время закрыть этот пробел.
Адаптивная вёрстка (responsive design) — это подход, при котором один и тот же HTML показывает себя по-разному в зависимости от размера экрана, на котором его открыли. Никакой отдельной «мобильной версии сайта» с другим адресом — один код, разное поведение CSS.
Сегодня мы возьмём страницу фильмов из Урока 11 и сделаем её адаптивной: на телефоне — одна колонка, на планшете — две, на десктопе — как было.
Откроем практический пример. Вот фрагмент шапки в духе того, что мы делали в Уроке 10:
<header class="header">
<div class="logo">MovieGrid</div>
<nav class="nav">
<a href="#">Главная</a>
<a href="#">Фильмы</a>
<a href="#">Контакты</a>
</nav>
</header>.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 32px;
background: #2d3436;
color: white;
}
.nav {
display: flex;
gap: 24px;
}На экране 1200px это выглядит отлично — логотип слева, навигация справа. На экране 375px (типичный телефон) слова начинают налезать друг на друга, а если добавить в навигацию ещё пару пунктов — шапка попросту "поедет" горизонтальным скроллом.
Без специального инструмента CSS не умеет менять поведение в зависимости от ширины экрана — свойства применяются одинаково всегда. Этот инструмент — медиа-запросы.
Прежде чем говорить о медиа-запросах, важно закрепить одну строку, которую вы уже видели в примерах прошлых уроков в <head>, но не разбирали подробно:
<meta name="viewport" content="width=device-width, initial-scale=1.0" />Без этого тега мобильные браузеры обманывают страницу: они рендерят её так, будто экран имеет ширину ~980px, а затем визуально сжимают результат, чтобы он поместился на маленький дисплей. Из-за этого весь текст становится нечитаемо мелким, а медиа-запросы, ориентированные на реальную ширину экрана, просто не будут срабатывать так, как вы ожидаете.
Разбор атрибутов:
| Значение | Что делает |
|---|---|
width=device-width |
Ширина области просмотра равна физической ширине экрана устройства |
initial-scale=1.0 |
Начальный масштаб — 1:1, без автоматического уменьшения |
Правило: этот тег должен присутствовать в <head> каждой адаптивной страницы. Без него всё, что мы разберём дальше, не будет работать корректно на реальных телефонах.
Медиа-запрос (@media) — это условие в CSS: «применяй эти стили, только если выполняется вот такое условие про экран».
<header class="header">
<div class="logo">MovieGrid</div>
<nav class="nav">
<a href="#">Главная</a>
<a href="#">Фильмы</a>
<a href="#">Контакты</a>
</nav>
</header>.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: #2d3436;
color: white;
}
.logo {
font-size: 22px;
font-weight: bold;
}
.nav {
display: flex;
gap: 20px;
}
.nav a {
color: white;
text-decoration: none;
}
@media (max-width: 768px) {
.header {
flex-direction: column;
align-items: flex-start;
}
.nav {
flex-direction: column;
gap: 10px;
margin-top: 12px;
}
}Здесь .header станет колонкой (элементы друг под другом), но только если ширина области просмотра 768px или меньше. На больших экранах это правило просто не применится, и сработают обычные стили .header из основного CSS.
| Условие | Срабатывает, когда |
|---|---|
max-width: 768px |
ширина экрана ≤ 768px |
min-width: 768px |
ширина экрана ≥ 768px |
max-height: 600px |
высота экрана ≤ 600px |
orientation: portrait |
экран выше, чем шире (телефон вертикально) |
orientation: landscape |
экран шире, чем выше |
В 95% практических задач вам понадобятся именно min-width и max-width — они отвечают за то, что реально называют "адаптивной вёрсткой". Остальные условия используются реже, для частных случаев.
Можно указать диапазон — стили применятся, только если ширина экрана попадает между двумя значениями:
@media (min-width: 768px) and (max-width: 1023px) {
/* только планшеты: от 768px до 1023px включительно */
}and — это логическое "И": оба условия должны быть истинны одновременно.
Медиа-запрос — это блок, который оборачивает обычные CSS-правила. Внутри него можно писать что угодно: свойства, селекторы, даже другие вложенные блоки не нужны — просто обычные правила, но действующие только при выполнении условия.
<div class="card">
<h3>Название фильма</h3>
<p>
Краткое описание фильма для демонстрации
медиа-запросов.
</p>
<a href="#">Подробнее</a>
</div>.card {
max-width: 320px;
padding: 16px;
border-radius: 8px;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.card h3 {
margin-top: 0;
}
.card a {
display: inline-block;
margin-top: 10px;
}
@media (max-width: 480px) {
.card {
padding: 8px;
}
}Важный момент: медиа-запрос не создаёт новый CSS с нуля — он переопределяет уже заданные свойства, если выполняется его условие. Порядок в файле имеет значение (как и в обычном каскаде CSS) — более поздние правила с тем же приоритетом побеждают более ранние.
Здесь принимается стратегическое решение, которое повлияет на весь ваш CSS: с какого экрана начинать писать стили — с маленького или с большого.
/* Базовые стили — рассчитаны на десктоп */
.container {
display: flex;
flex-direction: row;
}
/* Донастройка вниз, для меньших экранов */
@media (max-width: 1024px) {
.container {
flex-direction: column;
}
}
@media (max-width: 480px) {
.container {
gap: 8px;
}
}Мы пишем полноценную десктопную версию, а затем "откатываем" стили для меньших экранов через max-width.
/* Базовые стили — рассчитаны на мобильный экран */
.container {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Донастройка вверх, для больших экранов */
@media (min-width: 768px) {
.container {
flex-direction: row;
gap: 16px;
}
}Мы пишем базовую версию для самого маленького экрана, а затем "наращиваем" сложность и возможности вёрстки по мере роста экрана через min-width.
- Простота базового случая. На маленьком экране обычно меньше сложных раскладок — одна колонка, простая структура. Именно с простого удобнее начинать.
- Производительность. Мобильные устройства первыми получают минимальный, самый лёгкий набор стилей — без лишней сложной раскладки, которая им не нужна.
- Меньше противоречий в каскаде. С
min-widthвы только добавляете сложность по мере роста экрана. Сmax-widthвы вынуждены постоянно отменять то, что уже написали для десктопа — легче забыть что-то "переопределить назад" и получить баг. - Соответствует реальности трафика. На большинстве современных проектов мобильный трафик равен или превышает десктопный — разумно, чтобы "дефолтная", самая часто используемая версия сайта была лёгкой и рассчитанной именно на мобильный.
Итог: в этом курсе мы будем придерживаться mobile-first — начинаем с простого мобильного макета и добавляем min-width-запросы по мере роста экрана.
Брейкпоинт — это значение ширины экрана, на котором вёрстка меняет своё поведение (например, колонка становится строкой).
Частая ошибка новичков — искать "точные размеры экрана iPhone 15" и ставить брейкпоинт именно под это значение. Проблема в том, что моделей устройств тысячи, а их размеры экранов постоянно меняются с выходом новых линеек. Правильный подход — увеличивать ширину окна браузера до тех пор, пока макет не начнёт визуально "ломаться", и ставить брейкпоинт именно в этой точке — независимо от того, какое устройство имеет такую ширину.
Тем не менее, для старта удобно опираться на условные ориентиры, которые покрывают подавляющее большинство реальных экранов:
| Диапазон | Условное название | Типичные устройства |
|---|---|---|
| до 480px | Маленький мобильный | Телефоны в портретной ориентации |
| 480px – 767px | Мобильный | Телефоны, малые планшеты |
| 768px – 1023px | Планшет | iPad и аналоги |
| 1024px – 1279px | Малый десктоп / ноутбук | Ноутбуки, iPad landscape |
| 1280px и больше | Десктоп | Мониторы |
Эти цифры — не строгий стандарт, а ориентир. На практике в большинстве проектов достаточно 2–3 брейкпоинтов, а не всех пяти сразу — избыточное дробление усложняет поддержку CSS без реальной пользы.
<div class="cards">
<article class="card">Фильм 1</article>
<article class="card">Фильм 2</article>
<article class="card">Фильм 3</article>
<article class="card">Фильм 4</article>
<article class="card">Фильм 5</article>
<article class="card">Фильм 6</article>
</div>/* Mobile-first: базово — 1 колонка */
.cards {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
.card {
padding: 32px;
background: steelblue;
color: white;
text-align: center;
border-radius: 8px;
}
/* Планшет: 2 колонки */
@media (min-width: 768px) {
.cards {
grid-template-columns: repeat(2, 1fr);
}
}
/* Десктоп: 3 колонки */
@media (min-width: 1024px) {
.cards {
grid-template-columns: repeat(3, 1fr);
}
}Попробуйте постепенно изменять ширину окна браузера. Вы увидите, как количество колонок автоматически меняется с одной на две, а затем на три.
Мы разбирали единицы измерения в Уроке 8, но именно в контексте адаптивной вёрстки становится понятно, зачем они вообще нужны. Если вы верстаете фиксированными px значениями везде — адаптивность придётся строить целиком на медиа-запросах, переопределяя буквально каждое значение. Если вы используете относительные единицы с самого начала — многое подстроится "само", без единого @media.
| Единица | От чего зависит | Где уместна |
|---|---|---|
px |
Ничего, абсолютное значение | Border, тонкие декоративные детали |
% |
Размер родителя | Ширина колонок внутри контейнера |
rem |
Размер шрифта корневого элемента (html) |
Отступы, размеры шрифтов — масштабируются вместе с базовым размером |
em |
Размер шрифта текущего элемента | Внутренние отступы относительно текста конкретного блока |
vw / vh |
Ширина / высота окна просмотра (1vw = 1% ширины экрана) | Полноэкранные блоки, "резиновая" типографика |
clamp(min, preferred, max) — функция CSS, которая выбирает значение в заданном диапазоне: минимум, "желаемое" значение (обычно во vw), максимум. Она особенно полезна для типографики и отступов, потому что позволяет избежать медиа-запроса вообще там, где нужно плавное изменение, а не резкий скачок.
<section class="section">
<h1>MovieGrid</h1>
<p>
Размер заголовка и внутренние отступы этого блока
плавно изменяются вместе с шириной окна браузера.
</p>
</section>.section {
padding: clamp(16px, 4vw, 64px);
background: #f4f4f4;
border-radius: 8px;
}
h1 {
margin-top: 0;
/* не меньше 24px, не больше 48px,
а между ними — 5% ширины экрана */
font-size: clamp(24px, 5vw, 48px);
}На маленьком экране font-size будет 24px (упёрлось в минимум), на среднем — где-то между, плавно растёт вместе с шириной экрана, на большом — упрётся в 48px и дальше расти не будет.
Важно: clamp() не заменяет медиа-запросы полностью — он прекрасно решает задачу плавного масштабирования одного значения (шрифт, отступ, ширина блока), но не решает задачу структурной перестройки макета (например, превращения ряда карточек в колонку). Для структурных изменений медиа-запросы всё равно нужны.
Вернёмся к шапке из начала урока и сделаем её mobile-first.
<header class="header">
<div class="logo">MovieGrid</div>
<nav class="nav">
<a href="#">Главная</a>
<a href="#">Фильмы</a>
<a href="#">Сериалы</a>
<a href="#">Контакты</a>
</nav>
</header>/* Mobile-first: базовые стили рассчитаны на маленький экран */
.header {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
padding: 16px 20px;
background: #2d3436;
color: white;
}
.logo {
font-size: 20px;
font-weight: bold;
}
.nav {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.nav a {
color: white;
text-decoration: none;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}
/* Планшет и больше: шапка становится строкой */
@media (min-width: 768px) {
.header {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 16px 32px;
}
.nav {
flex-direction: row;
gap: 24px;
width: auto;
}
.nav a {
padding: 0;
border-bottom: none;
}
}Разбор: на телефоне логотип и пункты меню идут друг под другом на всю ширину экрана — с ними удобно взаимодействовать пальцем. На экране от 768px и шире шапка "разворачивается" в привычную горизонтальную строку — flex-direction: row возвращает поведение, знакомое по Уроку 10.
Попробуйте изменять ширину окна браузера. До 768px меню располагается столбцом, а после достижения этого значения автоматически перестраивается в горизонтальную строку.
Теперь применим тот же принцип к целой странице — той самой, что мы верстали в конце Урока 11 на grid-template-areas.
<header>
<h1>MovieGrid</h1>
</header>
<aside>
<h2>Фильтры</h2>
<ul>
<li>Боевики</li>
<li>Комедии</li>
<li>Фантастика</li>
<li>Драмы</li>
</ul>
</aside>
<main>
<div class="cards">
<article class="card">Фильм 1</article>
<article class="card">Фильм 2</article>
<article class="card">Фильм 3</article>
<article class="card">Фильм 4</article>
<article class="card">Фильм 5</article>
<article class="card">Фильм 6</article>
</div>
</main>
<footer>
© MovieGrid
</footer>Напомним десктопную структуру:
body {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 80px 1fr 60px;
grid-template-areas:
"header header"
"aside main"
"footer footer";
}На телефоне колонка в 250px под aside попросту не помещается рядом с контентом — а фильтры сбоку на маленьком экране вообще не самый удобный паттерн. Переверстаем по mobile-first: сначала — всё в один поток, друг под другом, потом при росте экрана — возвращаем двухколоночную структуру.
/* Mobile-first: все блоки идут один под другим */
body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
font-family: sans-serif;
}
header {
background: #6c5ce7;
color: white;
padding: 16px 20px;
}
aside {
background: #a29bfe;
color: white;
padding: 16px 20px;
}
aside ul {
margin: 12px 0 0;
padding-left: 20px;
}
main {
flex: 1;
padding: 16px 20px;
}
footer {
background: #636e72;
color: white;
padding: 16px 20px;
}
/* Карточки фильмов */
.cards {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
.card {
padding: 32px;
background: #f5f6fa;
border-radius: 8px;
text-align: center;
}
/* Планшет */
@media (min-width: 768px) {
body {
display: grid;
grid-template-columns: 200px 1fr;
grid-template-rows: 80px 1fr 60px;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
}
header {
grid-area: header;
}
aside {
grid-area: sidebar;
}
main {
grid-area: main;
}
footer {
grid-area: footer;
}
.cards {
grid-template-columns: repeat(2, 1fr);
}
}
/* Десктоп */
@media (min-width: 1024px) {
.cards {
grid-template-columns: repeat(3, 1fr);
}
}Ключевая идея этого примера: мы не просто "сжимаем" одну и ту же сетку — мы меняем сам тип раскладки. На мобильном body — это flex с колонкой из блоков друг под другом (Grid с areas тут не нужен, для одной колонки он избыточен). На планшете и десктопе body уже полноценный grid с areas, как в оригинале Урока 11. Медиа-запрос может переопределять не только частные свойства, но и саму стратегию раскладки контейнера.
Без <meta name="viewport" ...> все медиа-запросы, ориентированные на реальную ширину экрана, будут срабатывать некорректно на настоящих телефонах — хотя в инструментах разработчика в браузере всё может выглядеть нормально. Проверяйте этот тег в первую очередь, если адаптивность "не работает на телефоне, но работает в браузере".
/* Плохо: одни блоки используют min-width, другие — max-width */
@media (min-width: 768px) { .header { flex-direction: row; } }
@media (max-width: 767px) { .cards { grid-template-columns: 1fr; } }Технически это может сработать, но такой код сложно читать и поддерживать — приходится держать в голове две разные логики одновременно. Выберите одну стратегию (в этом курсе — mobile-first) и придерживайтесь её во всём проекте.
/* Излишне: пять брейкпоинтов для простой сетки карточек */
@media (min-width: 400px) { ... }
@media (min-width: 600px) { ... }
@media (min-width: 800px) { ... }
@media (min-width: 1000px) { ... }
@media (min-width: 1200px) { ... }Каждый новый брейкпоинт — это дополнительная точка, которую нужно тестировать и поддерживать. Начинайте с 2–3 брейкпоинтов и добавляйте новые только тогда, когда реально увидели, что макет "ломается" на промежуточной ширине.
/* Плохо: на любом экране от 768px карточка будет ровно 700px шириной */
@media (min-width: 768px) {
.card {
width: 700px;
}
}На экране 768px такая карточка вылезет за пределы экрана. Внутри медиа-запросов, как и вне них, предпочтительны относительные единицы (%, fr в Grid, flex: 1) — тогда блок будет корректно себя вести на любой ширине внутри диапазона, а не только на той единственной ширине, на которой вы тестировали.
Изменение ширины окна десктопного браузера — это не то же самое, что реальный телефон: разные пропорции экрана, разная плотность пикселей, разное поведение шрифтов и скроллбаров. Обязательно проверяйте вёрстку через "Инструменты разработчика" → режим эмуляции устройств (обычно иконка телефона/планшета в панели инструментов) — там можно выбрать конкретные модели устройств с реальными размерами экрана.
Адаптивная вёрстка строится на @media-запросах — блоках CSS-правил, которые применяются только при выполнении условия про ширину (реже — высоту или ориентацию) экрана. Обязательное условие корректной работы медиа-запросов на реальных устройствах — тег <meta name="viewport" content="width=device-width, initial-scale=1.0"> в <head>.
Мы выбрали стратегию mobile-first: базовые стили рассчитаны на маленький экран, а min-width-запросы постепенно добавляют сложность по мере роста экрана. Это противоположность desktop-first, где max-width-запросы, наоборот, "откатывают" уже написанные десктопные стили вниз.
Брейкпоинты — это не привязка к конкретным моделям устройств, а точки, где реальная вёрстка начинает визуально ломаться. На старте достаточно 2–3 брейкпоинтов (условно: мобильный / планшет / десктоп).
Относительные единицы (%, rem, vw) и функция clamp() снижают количество медиа-запросов, которые вообще придётся писать — многое может "подстроиться" плавно, без явных условий. Но структурные изменения раскладки (превращение колонки в строку, смена flex на grid) всё равно требуют явных медиа-запросов.
В следующем уроке разберём стилизацию input — не менее важную часть подготовки к формам, которыми мы закроем весь HTML/CSS-блок курса.
- Почему без тега
<meta name="viewport">медиа-запросы работают некорректно на реальных мобильных устройствах? - В чём разница между mobile-first и desktop-first подходами? Какой из них используется в этом курсе и почему?
- Как правильно выбирать значения для брейкпоинтов — по каким критериям?
- Что делает функция
clamp()и в каких случаях она не заменяет медиа-запрос? - В примере с адаптацией страницы фильмов (Урок 11) на мобильном экране
bodyиспользуетflex, а на планшете и десктопе —gridсgrid-template-areas. Почему нельзя было просто оставитьgridна всех экранах и просто менятьgrid-template-columns? - Чем плохо использование фиксированных
px-значений для ширины блоков внутри медиа-запроса?
Возьмите шапку сайта, которую вы создавали в самостоятельной практике Урока 10 (логотип, название, навигация). Сделайте её адаптивной по принципу mobile-first:
- На экране уже с ширины 0 (мобильный, база) — логотип и пункты навигации расположены вертикально, друг под другом, с отступами между ними.
- При достижении ширины 768px — шапка превращается в горизонтальную строку: логотип слева, навигация справа, вертикально выровнены по центру.
- Добавьте
<meta name="viewport">в<head>, если его там ещё нет. - Проверьте результат через инструменты разработчика в режиме эмуляции устройств, на 2–3 разных ширинах экрана.
Готовая html разметка
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Адаптивная шапка</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="header">
<div class="logo">MyLogo</div>
<nav class="nav">
<a href="#">Главная</a>
<a href="#">О нас</a>
<a href="#">Услуги</a>
<a href="#">Контакты</a>
</nav>
</header>
</body>
</html>Возьмите блок с карточками фильмов из практики Урока 11 (6 карточек: изображение, название, описание, кнопка). Сделайте сетку карточек адаптивной:
- Mobile-first: на маленьком экране — 1 колонка.
- На ширине от 600px — 2 колонки.
- На ширине от 1024px — 3 колонки.
- Отступы между карточками (
gap) на мобильном должны быть меньше, чем на десктопе — например, 12px против 24px.
Готовая html разметка
HTML (фрагмент, повторяется 6 раз):
<div class="cards">
<article class="card">
<img class="card__cover" src="img/movie1.jpg" alt="Название фильма" />
<h3 class="card__title">Название фильма</h3>
<p class="card__desc">Краткое описание фильма для примера карточки.</p>
<a class="card__link" href="#">Смотреть</a>
</article>
<!-- ...ещё 5 таких же карточек... -->
</div>Возьмите целиком страницу фильмов из финальной практики Урока 11 (header + aside + main с карточками + footer на grid-template-areas) и объедините решения заданий 1 и 2 в единый адаптивный документ:
- Ниже 768px:
body— простой вертикальный поток (flex-direction: column), никакогоgrid-template-areas. Шапка — как в Задании 1. Карточки — 1 колонка, как в Задании 2.asideс фильтрами располагается междуheaderиmain, на всю ширину. - От 768px до 1023px:
bodyпереключается наgridсgrid-template-areas(header header / aside main / footer footer),aside— 200px, карточки — 2 колонки. - От 1024px: карточки — 3 колонки, остальная структура как на планшете.
- Убедитесь, что переходы между всеми тремя состояниями визуально плавные — ничего не "прыгает" и не съезжает при изменении ширины окна.
Готовая html разметка
Вместо <article class="card">Фильм 1</article> подставить карточку из задания 2.
<header>
<h1>MovieGrid</h1>
</header>
<aside>
<h2>Фильтры</h2>
<ul>
<li>Боевики</li>
<li>Комедии</li>
<li>Фантастика</li>
<li>Драмы</li>
</ul>
</aside>
<main>
<div class="cards">
<article class="card">Фильм 1</article>
<article class="card">Фильм 2</article>
<article class="card">Фильм 3</article>
<article class="card">Фильм 4</article>
<article class="card">Фильм 5</article>
<article class="card">Фильм 6</article>
</div>
</main>
<footer>
© MovieGrid
</footer>