Медиа-запросы — это инструмент. Но инструмент бесполезен без стратегии. В этом уроке разберём проверенные паттерны, которые используются в реальных проектах: подход mobile-first, fluid-типографику, единицы вьюпорта и адаптивную навигацию.
Пишем базовые стили для мобильного, расширяем через min-width:
/* Базовые стили — мобильный */
.header {
padding: 12px 16px;
font-size: 1rem;
}
.nav-links {
display: none; /* На мобильном скрыта */
}
.hamburger {
display: block;
}
/* Планшет */
@media (min-width: 768px) {
.header {
padding: 16px 24px;
}
}
/* Десктоп */
@media (min-width: 1024px) {
.nav-links {
display: flex;
gap: 24px;
}
.hamburger {
display: none;
}
}Почему mobile-first лучше:
min-width работает каскадно — каждый следующий брейкпоинт дополняет предыдущий/* Базовые стили — десктоп */
.sidebar { width: 300px; float: left; }
/* «Ломаем» на планшетах */
@media (max-width: 1023px) {
.sidebar { width: 200px; }
}
/* «Ломаем» ещё раз на мобильных */
@media (max-width: 767px) {
.sidebar { width: 100%; float: none; }
}Desktop-first — это костыли поверх костылей. Каждый брейкпоинт отменяет предыдущие стили. Избегай.
Вместо прыжков размера шрифта на брейкпоинтах можно сделать плавное изменение.
/* clamp(минимум, предпочтительный, максимум) */
h1 {
font-size: clamp(1.5rem, 4vw + 0.5rem, 3.5rem);
}Как это работает:
1.5rem (24px)4vw + 0.5rem3.5rem (56px):root {
/* Fluid шрифты */
--text-sm: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
--text-base: clamp(0.875rem, 0.8rem + 0.4vw, 1rem);
--text-lg: clamp(1.125rem, 1rem + 0.6vw, 1.375rem);
--text-xl: clamp(1.25rem, 1rem + 1.2vw, 2rem);
--text-2xl: clamp(1.5rem, 1rem + 2vw, 3rem);
--text-3xl: clamp(2rem, 1.2rem + 3vw, 4rem);
/* Fluid отступы */
--space-sm: clamp(0.5rem, 0.3rem + 1vw, 1rem);
--space-md: clamp(1rem, 0.5rem + 2vw, 2rem);
--space-lg: clamp(1.5rem, 0.5rem + 4vw, 4rem);
--space-xl: clamp(2rem, 1rem + 5vw, 6rem);
}
h1 { font-size: var(--text-3xl); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-xl); }
p { font-size: var(--text-base); }
section { padding: var(--space-lg) var(--space-md); }Теперь вся типографика и отступы масштабируются плавно без единого медиа-запроса.
.hero {
width: 100vw; /* 100% ширины вьюпорта */
height: 100vh; /* 100% высоты вьюпорта */
}
.sidebar {
width: 25vw; /* 25% ширины */
min-height: 100vh;
}
.text {
font-size: 5vmin; /* 5% от меньшей стороны вьюпорта */
}| Единица | Что измеряет |
|---------|-------------|
| vw | 1% ширины вьюпорта |
| vh | 1% высоты вьюпорта |
| vmin | 1% меньшей стороны |
| vmax | 1% большей стороны |
На мобильных 100vh включает область за адресной строкой браузера. Результат — контент уезжает за пределы видимой области.
/* Плохо: на мобильном элемент выше видимой области */
.fullscreen {
height: 100vh;
}
/* Хорошо: новые динамические единицы */
.fullscreen {
height: 100dvh; /* Динамическая высота — учитывает состояние браузера */
}| Единица | Описание |
|---------|----------|
| svh / svw | Small viewport — минимальный размер (адресная строка видна) |
| lvh / lvw | Large viewport — максимальный размер (адресная строка скрыта) |
| dvh / dvw | Dynamic viewport — текущий размер (меняется при скролле) |
/* Герой-секция занимает ровно видимую область на любом устройстве */
.hero {
min-height: 100svh; /* Гарантированно помещается на экране */
}
/* Фиксированная панель внизу */
.bottom-bar {
position: fixed;
bottom: 0;
height: env(safe-area-inset-bottom, 0px); /* Учитывает «чёлку» iPhone */
}<nav class="nav">
<a href="/" class="nav-logo">Сайт</a>
<button class="nav-toggle" aria-label="Меню">☰</button>
<ul class="nav-links">
<li><a href="/about">О нас</a></li>
<li><a href="/services">Услуги</a></li>
<li><a href="/contacts">Контакты</a></li>
</ul>
</nav>.nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
}
.nav-links {
display: none;
list-style: none;
padding: 0;
margin: 0;
}
.nav-links.active {
display: flex;
flex-direction: column;
position: absolute;
top: 56px;
left: 0;
right: 0;
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 16px;
gap: 12px;
}
.nav-toggle {
display: block;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
@media (min-width: 768px) {
.nav-links {
display: flex;
flex-direction: row;
position: static;
background: none;
box-shadow: none;
padding: 0;
gap: 24px;
}
.nav-toggle {
display: none;
}
}.tabs {
display: flex;
overflow-x: auto; /* На мобильном — горизонтальный скролл */
-webkit-overflow-scrolling: touch;
scrollbar-width: none; /* Прячем скроллбар */
gap: 0;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
flex-shrink: 0; /* Не сжимаются */
padding: 12px 20px;
white-space: nowrap;
}
@media (min-width: 768px) {
.tabs {
overflow-x: visible; /* На десктопе всё видно */
justify-content: center;
}
}/* Плохо: «iPhone 14 Pro Max» */
@media (max-width: 430px) { }
/* Хорошо: контент определяет брейкпоинт */
/* Смотришь, где макет «ломается» — ставишь брейкпоинт */
@media (min-width: 600px) { } /* Контент перестаёт помещаться в одну колонку */
@media (min-width: 960px) { } /* Хватает места для сайдбара *//* Минимальный набор (2-3 брейкпоинта) */
:root {
--bp-tablet: 768px;
--bp-desktop: 1024px;
}
/* Или используй Container Queries (CSS 2023+) */
@container (min-width: 400px) {
.card { flex-direction: row; }
}Медиа-запросы проверяют размер окна. Container Queries — размер контейнера. Компонент адаптируется к своему контексту, а не к экрану.
.card-wrapper {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
.card-image {
width: 40%;
}
}
@container card (max-width: 399px) {
.card {
display: flex;
flex-direction: column;
}
.card-image {
width: 100%;
}
}Теперь карточка будет горизонтальной в широком контейнере и вертикальной в узком — независимо от размера экрана.
Ошибка 1: фиксированные размеры вместо fluid
/* Плохо */
h1 { font-size: 48px; }
@media (max-width: 768px) { h1 { font-size: 24px; } }
/* Хорошо */
h1 { font-size: clamp(1.5rem, 4vw + 0.5rem, 3rem); }Ошибка 2: 100vh на мобильном
/* Плохо: контент прячется за адресную строку */
.hero { height: 100vh; }
/* Хорошо */
.hero { min-height: 100dvh; }Ошибка 3: слишком много брейкпоинтов
Если у тебя больше 4 — значит, дизайн недостаточно гибкий. Fluid-размеры и flexbox/grid решают большинство задач без медиа-запросов.
Mobile-first страница с fluid-типографикой и адаптивной навигацией
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { margin: 0; box-sizing: border-box; }
:root {
--text-base: clamp(0.875rem, 0.8rem + 0.4vw, 1rem);
--text-xl: clamp(1.25rem, 1rem + 1.2vw, 2rem);
--text-3xl: clamp(2rem, 1.2rem + 3vw, 4rem);
--space-md: clamp(1rem, 0.5rem + 2vw, 2rem);
--space-lg: clamp(1.5rem, 0.5rem + 4vw, 4rem);
}
body { font-family: system-ui, sans-serif; font-size: var(--text-base); }
/* Навигация: mobile-first */
.nav {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #1e293b;
color: white;
}
.nav-logo { color: white; text-decoration: none; font-weight: 700; }
.nav-toggle {
display: block; background: none;
border: 1px solid #475569; color: white;
padding: 6px 12px; border-radius: 6px; cursor: pointer;
}
.nav-links {
display: none; width: 100%;
list-style: none; padding: 12px 0 0; margin: 0;
}
.nav-links.active { display: flex; flex-direction: column; gap: 8px; }
.nav-links a { color: #94a3b8; text-decoration: none; }
@media (min-width: 768px) {
.nav-toggle { display: none; }
.nav-links {
display: flex !important; width: auto;
flex-direction: row; gap: 24px; padding: 0;
}
}
/* Hero: fluid */
.hero {
min-height: 100dvh;
display: flex; align-items: center; justify-content: center;
padding: var(--space-lg);
background: linear-gradient(135deg, #0f172a, #1e3a5f);
color: white; text-align: center;
}
.hero h1 { font-size: var(--text-3xl); margin-bottom: var(--space-md); }
.hero p { font-size: var(--text-xl); opacity: 0.8; max-width: 600px; }
/* Контент */
.content {
max-width: 1200px;
margin: 0 auto;
padding: var(--space-lg) var(--space-md);
}
</style>
</head>
<body>
<nav class="nav">
<a href="/" class="nav-logo">MySite</a>
<button class="nav-toggle" onclick="this.nextElementSibling.classList.toggle('active')">☰</button>
<ul class="nav-links">
<li><a href="#">Главная</a></li>
<li><a href="#">О нас</a></li>
<li><a href="#">Контакты</a></li>
</ul>
</nav>
<section class="hero">
<div>
<h1>Fluid типографика</h1>
<p>Размер текста плавно меняется от мобильного к десктопу — без единого медиа-запроса</p>
</div>
</section>
<div class="content">
<h2 style="font-size: var(--text-xl); margin-bottom: var(--space-md);">
Измени размер окна
</h2>
<p>Навигация превращается в гамбургер-меню, типографика масштабируется, герой-секция занимает ровно видимый экран.</p>
</div>
</body>
</html>Медиа-запросы — это инструмент. Но инструмент бесполезен без стратегии. В этом уроке разберём проверенные паттерны, которые используются в реальных проектах: подход mobile-first, fluid-типографику, единицы вьюпорта и адаптивную навигацию.
Пишем базовые стили для мобильного, расширяем через min-width:
/* Базовые стили — мобильный */
.header {
padding: 12px 16px;
font-size: 1rem;
}
.nav-links {
display: none; /* На мобильном скрыта */
}
.hamburger {
display: block;
}
/* Планшет */
@media (min-width: 768px) {
.header {
padding: 16px 24px;
}
}
/* Десктоп */
@media (min-width: 1024px) {
.nav-links {
display: flex;
gap: 24px;
}
.hamburger {
display: none;
}
}Почему mobile-first лучше:
min-width работает каскадно — каждый следующий брейкпоинт дополняет предыдущий/* Базовые стили — десктоп */
.sidebar { width: 300px; float: left; }
/* «Ломаем» на планшетах */
@media (max-width: 1023px) {
.sidebar { width: 200px; }
}
/* «Ломаем» ещё раз на мобильных */
@media (max-width: 767px) {
.sidebar { width: 100%; float: none; }
}Desktop-first — это костыли поверх костылей. Каждый брейкпоинт отменяет предыдущие стили. Избегай.
Вместо прыжков размера шрифта на брейкпоинтах можно сделать плавное изменение.
/* clamp(минимум, предпочтительный, максимум) */
h1 {
font-size: clamp(1.5rem, 4vw + 0.5rem, 3.5rem);
}Как это работает:
1.5rem (24px)4vw + 0.5rem3.5rem (56px):root {
/* Fluid шрифты */
--text-sm: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
--text-base: clamp(0.875rem, 0.8rem + 0.4vw, 1rem);
--text-lg: clamp(1.125rem, 1rem + 0.6vw, 1.375rem);
--text-xl: clamp(1.25rem, 1rem + 1.2vw, 2rem);
--text-2xl: clamp(1.5rem, 1rem + 2vw, 3rem);
--text-3xl: clamp(2rem, 1.2rem + 3vw, 4rem);
/* Fluid отступы */
--space-sm: clamp(0.5rem, 0.3rem + 1vw, 1rem);
--space-md: clamp(1rem, 0.5rem + 2vw, 2rem);
--space-lg: clamp(1.5rem, 0.5rem + 4vw, 4rem);
--space-xl: clamp(2rem, 1rem + 5vw, 6rem);
}
h1 { font-size: var(--text-3xl); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-xl); }
p { font-size: var(--text-base); }
section { padding: var(--space-lg) var(--space-md); }Теперь вся типографика и отступы масштабируются плавно без единого медиа-запроса.
.hero {
width: 100vw; /* 100% ширины вьюпорта */
height: 100vh; /* 100% высоты вьюпорта */
}
.sidebar {
width: 25vw; /* 25% ширины */
min-height: 100vh;
}
.text {
font-size: 5vmin; /* 5% от меньшей стороны вьюпорта */
}| Единица | Что измеряет |
|---------|-------------|
| vw | 1% ширины вьюпорта |
| vh | 1% высоты вьюпорта |
| vmin | 1% меньшей стороны |
| vmax | 1% большей стороны |
На мобильных 100vh включает область за адресной строкой браузера. Результат — контент уезжает за пределы видимой области.
/* Плохо: на мобильном элемент выше видимой области */
.fullscreen {
height: 100vh;
}
/* Хорошо: новые динамические единицы */
.fullscreen {
height: 100dvh; /* Динамическая высота — учитывает состояние браузера */
}| Единица | Описание |
|---------|----------|
| svh / svw | Small viewport — минимальный размер (адресная строка видна) |
| lvh / lvw | Large viewport — максимальный размер (адресная строка скрыта) |
| dvh / dvw | Dynamic viewport — текущий размер (меняется при скролле) |
/* Герой-секция занимает ровно видимую область на любом устройстве */
.hero {
min-height: 100svh; /* Гарантированно помещается на экране */
}
/* Фиксированная панель внизу */
.bottom-bar {
position: fixed;
bottom: 0;
height: env(safe-area-inset-bottom, 0px); /* Учитывает «чёлку» iPhone */
}<nav class="nav">
<a href="/" class="nav-logo">Сайт</a>
<button class="nav-toggle" aria-label="Меню">☰</button>
<ul class="nav-links">
<li><a href="/about">О нас</a></li>
<li><a href="/services">Услуги</a></li>
<li><a href="/contacts">Контакты</a></li>
</ul>
</nav>.nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
}
.nav-links {
display: none;
list-style: none;
padding: 0;
margin: 0;
}
.nav-links.active {
display: flex;
flex-direction: column;
position: absolute;
top: 56px;
left: 0;
right: 0;
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 16px;
gap: 12px;
}
.nav-toggle {
display: block;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
@media (min-width: 768px) {
.nav-links {
display: flex;
flex-direction: row;
position: static;
background: none;
box-shadow: none;
padding: 0;
gap: 24px;
}
.nav-toggle {
display: none;
}
}.tabs {
display: flex;
overflow-x: auto; /* На мобильном — горизонтальный скролл */
-webkit-overflow-scrolling: touch;
scrollbar-width: none; /* Прячем скроллбар */
gap: 0;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
flex-shrink: 0; /* Не сжимаются */
padding: 12px 20px;
white-space: nowrap;
}
@media (min-width: 768px) {
.tabs {
overflow-x: visible; /* На десктопе всё видно */
justify-content: center;
}
}/* Плохо: «iPhone 14 Pro Max» */
@media (max-width: 430px) { }
/* Хорошо: контент определяет брейкпоинт */
/* Смотришь, где макет «ломается» — ставишь брейкпоинт */
@media (min-width: 600px) { } /* Контент перестаёт помещаться в одну колонку */
@media (min-width: 960px) { } /* Хватает места для сайдбара *//* Минимальный набор (2-3 брейкпоинта) */
:root {
--bp-tablet: 768px;
--bp-desktop: 1024px;
}
/* Или используй Container Queries (CSS 2023+) */
@container (min-width: 400px) {
.card { flex-direction: row; }
}Медиа-запросы проверяют размер окна. Container Queries — размер контейнера. Компонент адаптируется к своему контексту, а не к экрану.
.card-wrapper {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
.card-image {
width: 40%;
}
}
@container card (max-width: 399px) {
.card {
display: flex;
flex-direction: column;
}
.card-image {
width: 100%;
}
}Теперь карточка будет горизонтальной в широком контейнере и вертикальной в узком — независимо от размера экрана.
Ошибка 1: фиксированные размеры вместо fluid
/* Плохо */
h1 { font-size: 48px; }
@media (max-width: 768px) { h1 { font-size: 24px; } }
/* Хорошо */
h1 { font-size: clamp(1.5rem, 4vw + 0.5rem, 3rem); }Ошибка 2: 100vh на мобильном
/* Плохо: контент прячется за адресную строку */
.hero { height: 100vh; }
/* Хорошо */
.hero { min-height: 100dvh; }Ошибка 3: слишком много брейкпоинтов
Если у тебя больше 4 — значит, дизайн недостаточно гибкий. Fluid-размеры и flexbox/grid решают большинство задач без медиа-запросов.
Mobile-first страница с fluid-типографикой и адаптивной навигацией
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { margin: 0; box-sizing: border-box; }
:root {
--text-base: clamp(0.875rem, 0.8rem + 0.4vw, 1rem);
--text-xl: clamp(1.25rem, 1rem + 1.2vw, 2rem);
--text-3xl: clamp(2rem, 1.2rem + 3vw, 4rem);
--space-md: clamp(1rem, 0.5rem + 2vw, 2rem);
--space-lg: clamp(1.5rem, 0.5rem + 4vw, 4rem);
}
body { font-family: system-ui, sans-serif; font-size: var(--text-base); }
/* Навигация: mobile-first */
.nav {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #1e293b;
color: white;
}
.nav-logo { color: white; text-decoration: none; font-weight: 700; }
.nav-toggle {
display: block; background: none;
border: 1px solid #475569; color: white;
padding: 6px 12px; border-radius: 6px; cursor: pointer;
}
.nav-links {
display: none; width: 100%;
list-style: none; padding: 12px 0 0; margin: 0;
}
.nav-links.active { display: flex; flex-direction: column; gap: 8px; }
.nav-links a { color: #94a3b8; text-decoration: none; }
@media (min-width: 768px) {
.nav-toggle { display: none; }
.nav-links {
display: flex !important; width: auto;
flex-direction: row; gap: 24px; padding: 0;
}
}
/* Hero: fluid */
.hero {
min-height: 100dvh;
display: flex; align-items: center; justify-content: center;
padding: var(--space-lg);
background: linear-gradient(135deg, #0f172a, #1e3a5f);
color: white; text-align: center;
}
.hero h1 { font-size: var(--text-3xl); margin-bottom: var(--space-md); }
.hero p { font-size: var(--text-xl); opacity: 0.8; max-width: 600px; }
/* Контент */
.content {
max-width: 1200px;
margin: 0 auto;
padding: var(--space-lg) var(--space-md);
}
</style>
</head>
<body>
<nav class="nav">
<a href="/" class="nav-logo">MySite</a>
<button class="nav-toggle" onclick="this.nextElementSibling.classList.toggle('active')">☰</button>
<ul class="nav-links">
<li><a href="#">Главная</a></li>
<li><a href="#">О нас</a></li>
<li><a href="#">Контакты</a></li>
</ul>
</nav>
<section class="hero">
<div>
<h1>Fluid типографика</h1>
<p>Размер текста плавно меняется от мобильного к десктопу — без единого медиа-запроса</p>
</div>
</section>
<div class="content">
<h2 style="font-size: var(--text-xl); margin-bottom: var(--space-md);">
Измени размер окна
</h2>
<p>Навигация превращается в гамбургер-меню, типографика масштабируется, герой-секция занимает ровно видимый экран.</p>
</div>
</body>
</html>Создай mobile-first страницу с fluid-типографикой. Заголовок `h1` должен плавно масштабироваться от `1.5rem` до `3.5rem` с помощью `clamp()`. Герой-секция должна занимать всю высоту видимого экрана (используй динамическую единицу вьюпорта). Добавь CSS-переменные для fluid-отступов.
Для `--text-heading` используй `clamp(1.5rem, 1.2rem + 3vw, 3.5rem)`. Динамическая единица высоты вьюпорта — `dvh`, то есть `100dvh`. В `clamp()` для отступов средний аргумент содержит сумму `rem` и `vw`. Переменные подставляй через `var(--имя)`.