CSS Layout — Flexbox, Grid и адаптивная вёрстка
CSS Layout — набор механизмов CSS для размещения элементов, распределения свободного пространства и адаптации интерфейса к разным размерам экрана.
Основные инструменты:
- Flexbox — одномерная раскладка по строке или столбцу;
- CSS Grid — двумерная раскладка по строкам и колонкам;
- медиазапросы — изменение стилей в зависимости от условий;
- относительные единицы и CSS-функции — плавное изменение размеров;
- позиционирование — закрепление и наложение отдельных элементов.
Flexbox и Grid дополняют друг друга. Grid обычно используют для общей структуры страницы и сеток, а Flexbox — для расположения элементов внутри компонентов.
Содержание
- Flexbox
- CSS Grid
- Flexbox или Grid
- Адаптивная вёрстка
- Адаптивные единицы и функции
- Сложные layout-паттерны
- CSS-переменные в layout
- Управление наложением
- Адаптивная типографика
- Адаптивные изображения
- Общий пример
- Частые ошибки
- Краткая памятка
- Итоги
Flexbox
Flexbox — одномерная модель раскладки. Она управляет элементами преимущественно вдоль одной оси: горизонтальной или вертикальной.
.container {
display: flex;
}Непосредственные дочерние элементы становятся flex-элементами.
Главная и поперечная оси
Во Flexbox используются две оси:
- главная ось — направление размещения элементов;
- поперечная ось — направление, перпендикулярное главной.
Направление главной оси задаёт flex-direction. Поэтому действие justify-content и align-items зависит от выбранного направления.
flex-direction
.container {
display: flex;
flex-direction: row;
}| Значение | Поведение |
|---|---|
row |
Элементы располагаются в строку по направлению текста |
row-reverse |
Элементы располагаются в обратном направлении строки |
column |
Элементы располагаются сверху вниз |
column-reverse |
Элементы располагаются снизу вверх |
.navigation {
display: flex;
flex-direction: row;
}
.form {
display: flex;
flex-direction: column;
}row-reverse и column-reverse меняют визуальный порядок, но не должны использоваться для исправления неправильной семантической структуры HTML.
justify-content
Распределяет элементы вдоль главной оси:
.container {
display: flex;
justify-content: space-between;
}| Значение | Поведение |
|---|---|
flex-start |
Элементы прижаты к началу оси |
flex-end |
Элементы прижаты к концу оси |
center |
Элементы собраны по центру |
space-between |
Свободное место распределено между элементами |
space-around |
Вокруг каждого элемента создаётся пространство |
space-evenly |
Все промежутки имеют одинаковый размер |
.header {
display: flex;
align-items: center;
justify-content: space-between;
}justify-content распределяет только оставшееся свободное пространство.
align-items
Выравнивает элементы вдоль поперечной оси:
.container {
display: flex;
align-items: center;
}| Значение | Поведение |
|---|---|
stretch |
Элементы растягиваются, если их размер не задан |
flex-start |
Выравнивание по началу оси |
flex-end |
Выравнивание по концу оси |
center |
Центрирование |
baseline |
Выравнивание по базовой линии текста |
Центрирование по обеим осям:
.centered {
display: flex;
min-height: 300px;
align-items: center;
justify-content: center;
}Для отдельного flex-элемента выравнивание переопределяют через align-self:
.item--end {
align-self: flex-end;
}flex-wrap
По умолчанию элементы стараются разместиться в одной строке:
.container {
display: flex;
flex-wrap: nowrap;
}Чтобы разрешить перенос:
.container {
display: flex;
flex-wrap: wrap;
}| Значение | Поведение |
|---|---|
nowrap |
Все элементы остаются на одной линии |
wrap |
Элементы переносятся на новые линии |
wrap-reverse |
Линии переносятся в обратном направлении |
Краткая запись для направления и переноса:
.container {
display: flex;
flex-flow: row wrap;
}gap
Задаёт расстояние между элементами без внешних отступов по краям контейнера:
.container {
display: flex;
flex-wrap: wrap;
gap: 16px;
}.container {
row-gap: 24px;
column-gap: 16px;
}Краткая запись gap: 24px 16px задаёт сначала расстояние между строками, затем между колонками.
flex-grow
Определяет, какую долю свободного пространства может получить элемент:
.item {
flex-grow: 1;
}Если всем элементам задать flex-grow: 1, свободное место будет распределено поровну. Разные коэффициенты задают пропорцию:
.sidebar {
flex-grow: 1;
}
.content {
flex-grow: 3;
}flex-grow распределяет свободное пространство с учётом базовых размеров, поэтому итоговая ширина не обязана быть ровно в заданной пропорции.
flex-shrink
Определяет, насколько элемент может уменьшаться при нехватке места:
.item {
flex-shrink: 1;
}
.avatar {
flex-shrink: 0;
width: 64px;
height: 64px;
}Значение по умолчанию — 1. Чем больше коэффициент, тем активнее элемент участвует в сжатии.
flex-basis
Задаёт исходный размер элемента вдоль главной оси до распределения свободного пространства:
.sidebar {
flex-basis: 280px;
}При flex-direction: row это исходная ширина, а при column — исходная высота.
.item {
flex-basis: 0;
flex-grow: 1;
}flex-basis: 0 позволяет распределять доступное место преимущественно по коэффициентам flex-grow.
Краткое свойство flex
flex объединяет flex-grow, flex-shrink и flex-basis:
.item {
flex: 1 1 240px;
}Эквивалентно:
.item {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 240px;
}Частые варианты:
/* Базовый размер 0, может расти и сжиматься */
.item {
flex: 1;
}
/* Фиксированный элемент */
.sidebar {
flex: 0 0 240px;
}
/* Гибкий элемент с базовым размером */
.card {
flex: 1 1 280px;
}Автоматический внешний отступ
В Flexbox margin: auto поглощает свободное пространство вдоль соответствующей оси:
.toolbar {
display: flex;
align-items: center;
}
.toolbar__actions {
margin-inline-start: auto;
}Переполнение flex-элементов
Flex-элемент может не сжиматься меньше минимальной ширины своего содержимого. Для гибкой колонки часто нужен min-width: 0:
.layout {
display: flex;
}
.content {
min-width: 0;
}Для вертикального flex-контейнера аналогичная проблема может потребовать min-height: 0.
Обрезка длинной строки:
.title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}Когда использовать Flexbox
Flexbox удобен для навигации, панелей инструментов, групп кнопок, выравнивания иконки и текста, центрирования содержимого и раскладки по одной основной оси.
CSS Grid
CSS Grid — двумерная система раскладки, которая одновременно управляет колонками и строками.
.grid {
display: grid;
}grid-template-columns
Определяет количество и размеры колонок:
.grid {
display: grid;
grid-template-columns: 200px 1fr 1fr;
}Одинаковые колонки удобно описывать через repeat():
.grid {
grid-template-columns: repeat(3, 1fr);
}grid-template-rows
Определяет размеры явно созданных строк:
.page {
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}Неявно создаваемые строки настраиваются через grid-auto-rows:
.grid {
grid-auto-rows: minmax(120px, auto);
}Единица fr
fr представляет долю свободного пространства Grid-контейнера:
.layout {
display: grid;
grid-template-columns: 1fr 2fr;
}После учёта фиксированных размеров и промежутков вторая колонка получит в два раза больше оставшегося пространства.
Для защиты от переполнеия длинным содержимым используют minmax(0, 1fr):
.layout {
grid-template-columns: 240px minmax(0, 1fr);
}gap
.grid {
display: grid;
gap: 24px;
}Разные промежутки:
.grid {
row-gap: 32px;
column-gap: 20px;
}gap не добавляет внешние отступы по краям сетки. Для них используется padding контейнера.
minmax()
Задаёт минимальный и максимальный размер трека:
.grid {
grid-template-columns: repeat(3, minmax(200px, 1fr));
}Колонка не станет уже 200px, но может расширяться до своей доли свободного пространства. В адаптивной сетке minmax() обычно объединяют с auto-fit или auto-fill.
auto-fill и auto-fit
auto-fill создаёт столько колонок, сколько помещается в контейнер, и сохраняет возможные пустые треки:
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 20px;
}auto-fit также создаёт подходящее число колонок, но схлопывает пустые треки и позволяет существующим элементам растянуться:
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
}Практическое различие:
auto-fillсохраняет структуру потенциальных колонок;auto-fitрастягивает существующие элементы.
Для карточной сетки чаще выбирают auto-fit.
Размещение по линиям
Grid-линии нумеруются начиная с 1:
.featured {
grid-column: 1 / 3;
}
.item {
grid-row: 2 / 4;
}Краткая запись:
.featured {
grid-column: span 2;
}
.banner {
grid-column: 1 / -1;
}-1 обозначает последнюю явную grid-линию.
grid-template-areas
Именованные области позволяют описать структуру макета:
.page {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
min-height: 100vh;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }Каждая строка шаблона должна содержать одинаковое количество ячеек, а повторяющиеся имена должны образовывать прямоугольную область. Точка обозначает пустую ячейку:
.layout {
grid-template-areas:
"header header"
". main";
}Сетка может менять шаблон в медиазапросе:
.page {
display: grid;
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
}
@media (min-width: 768px) {
.page {
grid-template-columns: 240px 1fr;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
}
}Визуальное переставление областей не должно разрушать логический порядок HTML и клавиатурной навигации.
Выравнивание в Grid
Содержимое внутри ячеек:
.grid {
justify-items: center;
align-items: center;
}
/* Краткая запись */
.grid {
place-items: center;
}Сама сетка внутри контейнера:
.grid {
justify-content: center;
align-content: start;
}Для отдельного элемента:
.item {
justify-self: end;
align-self: center;
}Когда использовать Grid
Grid удобен для общей структуры страницы, макетов с сайдбаром, карточных и галерейных сеток, а также раскладок, где важны обе оси.
Flexbox или Grid
| Задача | Предпочтительный инструмент |
|---|---|
| Элементы идут по одной основной оси | Flexbox |
| Нужны строки и колонки одновременно | Grid |
| Навигация или группа кнопок | Flexbox |
| Карточная сетка | Grid |
| Выравнивание иконки и текста | Flexbox |
| Страница с header, sidebar, main и footer | Grid |
| Центрирование одного элемента | Flexbox или Grid |
Оба механизма можно вкладывать:
.page {
display: grid;
grid-template-columns: 240px 1fr;
}
.card__actions {
display: flex;
align-items: center;
justify-content: space-between;
}Адаптивная вёрстка
Адаптивная вёрстка изменяет расположение, размеры и представление элементов в зависимости от доступного пространства и пользовательских настроек.
Для мобильных устройств нужен viewport:
<meta name="viewport" content="width=device-width, initial-scale=1.0">Медиазапросы
@media (min-width: 768px) {
.layout {
grid-template-columns: 240px 1fr;
}
}Частые условия:
@media (min-width: 768px) { /* ширина не меньше */ }
@media (max-width: 767px) { /* ширина не больше */ }
@media (orientation: landscape) { /* альбомная ориентация */ }
@media (hover: hover) and (pointer: fine) { /* точный указатель */ }Современный диапазонный синтаксис:
@media (width >= 768px) {
/* ... */
}
@media (480px <= width < 1024px) {
/* ... */
}Условия можно объединять через and, а альтернативы — через запятую:
@media (min-width: 768px) and (orientation: landscape) {
/* Оба условия */
}
@media (max-width: 480px), (orientation: portrait) {
/* Достаточно одного условия */
}Mobile-first
Mobile-first означает, что базовые стили создаются для узкого экрана, а затем расширяются через min-width:
.cards {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
@media (min-width: 600px) {
.cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 960px) {
.cards {
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
}Преимущества:
- сначала задаётся простая базовая раскладка;
- улучшения добавляются по мере появления пространства;
- меньше переопределений для мобильной версии;
- проще контролировать усложнение интерфейса.
Desktop-first использует max-width:
.layout {
grid-template-columns: 280px 1fr;
}
@media (max-width: 767px) {
.layout {
grid-template-columns: 1fr;
}
}Выбор брейкпоинтов
Брейкпоинты выбирают по поведению содержимого, а не по конкретным моделям устройств:
- начать с узкой версии;
- постепенно увеличивать ширину окна;
- найти момент, когда содержимое становится неудобным;
- добавить брейкпоинт, исправляющий раскладку.
/* Базовые стили */
@media (min-width: 40rem) {
/* Средние экраны */
}
@media (min-width: 64rem) {
/* Широкие экраны */
}Предпочтения пользователя
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation: none;
transition: none;
scroll-behavior: auto;
}
}@media (prefers-color-scheme: dark) {
:root {
--color-background: #111827;
--color-text: #f9fafb;
}
}Container queries
Медиазапрос реагирует на область просмотра, а container query — на ширину контейнера компонента:
.card-wrapper {
container-type: inline-size;
}
.card {
display: grid;
gap: 16px;
}
@container (min-width: 480px) {
.card {
grid-template-columns: 160px 1fr;
}
}Container queries полезны для переиспользуемых компонентов, находящихся в контейнерах разной ширины.
Адаптивные единицы и функции
%
Процент рассчитывается относительно значения, зависящего от свойства и контекста:
.image {
width: 100%;
}При наличии gap две колонки по 50% вместе с промежутком могут переполнить контейнер:
/* Потенциальная проблема */
.columns {
display: flex;
gap: 24px;
}
.column {
width: 50%;
}Надёжнее использовать Flexbox с flex или Grid:
.columns {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
}vw и vh
1vw равен одному проценту ширины области просмотра, а 1vh — одному проценту высоты:
.hero {
min-height: 100vh;
}
.title {
font-size: 5vw;
}Чистый vw для текста может дать слишком маленький или большой шрифт, поэтому его обычно ограничивают через clamp().
Современные единицы высоты:
| Единица | Значение |
|---|---|
svh |
Высота малой области просмотра |
lvh |
Высота большой области просмотра |
dvh |
Динамическая текущая высота |
.full-screen {
min-height: 100dvh;
}clamp()
Ограничивает значение минимумом и максимумом:
.title {
font-size: clamp(2rem, 5vw, 4rem);
}Синтаксис: clamp(minimum, preferred, maximum).
minimum— нижняя граница;preferred— изменяемое предпочтительное значение;maximum— верхняя граница.
.section {
padding-block: clamp(3rem, 8vw, 8rem);
padding-inline: clamp(1rem, 4vw, 3rem);
}min()
Выбирает наименьшее значение:
.container {
width: min(100% - 2rem, 75rem);
margin-inline: auto;
}
.card {
width: min(100%, 500px);
}max()
Выбирает наибольшее значение:
.section {
padding-inline: max(1rem, 5vw);
}Отступ с учётом безопасной зоны устройства:
.header {
padding-top: max(1rem, env(safe-area-inset-top));
}calc()
Позволяет объединять разные единицы:
.main {
min-height: calc(100dvh - 64px);
}
.sidebar {
width: calc(30% - 1rem);
}В выражениях вокруг + и - нужны пробелы: calc(100% - 2rem).
Сложные layout-паттерны
Sticky header
Закреплённая шапка остаётся у верхней границы во время прокрутки:
.site-header {
position: sticky;
z-index: 100;
top: 0;
background-color: rgb(255 255 255 / 90%);
border-bottom: 1px solid #e5e7eb;
backdrop-filter: blur(12px);
}Для работы sticky:
- должна быть задана граница, например
top: 0; - элемент должен иметь пространство для прокрутки;
overflowу предков может изменить scroll-контейнер;- высота родителя может ограничить область закрепления.
Если шапка перекрывает заголовок после перехода по якорю:
html {
scroll-padding-top: 80px;
}
.section {
scroll-margin-top: 80px;
}Sidebar
Mobile-first вариант на Grid:
.layout {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
}
@media (min-width: 768px) {
.layout {
grid-template-columns: 240px minmax(0, 1fr);
}
}Сайдбар с ограниченно гибкой шириной:
.layout {
display: grid;
grid-template-columns:
minmax(200px, 280px)
minmax(0, 1fr);
gap: clamp(1.5rem, 4vw, 4rem);
}Sticky-содержимое сайдбара:
.sidebar {
align-self: start;
}
.sidebar__inner {
position: sticky;
top: 96px;
}Закреплять часто удобнее внутренний блок, а не весь grid-элемент.
Вариант на Flexbox:
.layout {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.sidebar {
flex: 1 1 220px;
}
.main {
flex: 999 1 600px;
min-width: 0;
}При недостатке места блоки переносятся на разные строки.
Карточная сетка
Адаптивная сетка без медиазапросов:
.cards {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(100%, 260px), 1fr)
);
gap: clamp(1rem, 2vw, 1.5rem);
}min(100%, 260px) не позволяет минимальной ширине колонки вызвать переполнение на очень узком экране.
Карточка с кнопкой у нижнего края:
.card {
display: flex;
min-width: 0;
flex-direction: column;
}
.card__content {
display: flex;
flex: 1;
flex-direction: column;
padding: 20px;
}
.card__actions {
margin-top: auto;
padding-top: 20px;
}Выделенная карточка на широком экране:
@media (min-width: 900px) {
.card--featured {
grid-column: span 2;
}
}Страница с прижатым подвалом
.page {
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100dvh;
}Средняя строка занимает свободное место, поэтому подвал остаётся у нижнего края короткой страницы без position: fixed.
Раскладка с именованными областями
.page {
display: grid;
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"aside"
"footer";
min-height: 100dvh;
}
.page__header { grid-area: header; }
.page__main { grid-area: main; min-width: 0; }
.page__aside { grid-area: aside; }
.page__footer { grid-area: footer; }
@media (min-width: 900px) {
.page {
grid-template-columns: minmax(0, 1fr) 280px;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"main aside"
"footer footer";
}
}CSS-переменные в layout
CSS custom properties хранят повторяющиеся размеры, промежутки, цвета и уровни слоёв:
:root {
--container-max: 75rem;
--page-padding: 1rem;
--sidebar-width: 16rem;
--space-1: 0.5rem;
--space-2: 1rem;
--space-3: 1.5rem;
--space-4: 2rem;
}Использование:
.container {
width: min(
100% - 2 * var(--page-padding),
var(--container-max)
);
margin-inline: auto;
}
.layout {
display: grid;
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
gap: var(--space-4);
}Адаптивное переопределение
:root {
--page-padding: 1rem;
--section-space: 3rem;
}
@media (min-width: 768px) {
:root {
--page-padding: 2rem;
--section-space: 5rem;
}
}Во многих случаях значения можно менять плавно:
:root {
--page-padding: clamp(1rem, 4vw, 3rem);
--section-space: clamp(3rem, 8vw, 8rem);
}Локальные переменные компонента
.card {
--card-padding: 1.25rem;
--card-columns: 1fr;
display: grid;
grid-template-columns: var(--card-columns);
padding: var(--card-padding);
}
.card--compact {
--card-padding: 0.75rem;
}
@container (min-width: 480px) {
.card--horizontal {
--card-columns: 160px 1fr;
}
}Резервное значение:
.component {
gap: var(--component-gap, 1rem);
}Оно используется, если переменная не определена или недействительна.
Переменные для слоёв
:root {
--z-base: 0;
--z-dropdown: 100;
--z-sticky: 200;
--z-overlay: 300;
--z-modal: 400;
--z-toast: 500;
}
.modal {
z-index: var(--z-modal);
}Переменные не отменяют правила stacking context: большое значение внутри нижнего контекста не поднимет элемент над соседним верхним контекстом.
Управление наложением
z-index
z-index задаёт порядок отображения элементов по оси, направленной к пользователю:
.dropdown {
position: absolute;
z-index: 100;
}Большее число не гарантирует отображение поверх всех элементов. Сначала сравниваются стекинг-контексты, затем элементы внутри одного контекста.
Stacking context
Stacking context — независимая группа слоёв. Дочерний элемент не может выйти по z-index за границы контекста родителя.
.panel {
position: relative;
z-index: 1;
}
.tooltip {
position: absolute;
z-index: 9999;
}
.header {
position: relative;
z-index: 2;
}.tooltip останется внутри контекста .panel. Весь .panel расположен ниже .header, несмотря на z-index: 9999 у подсказки.
Что создаёт stacking context
Частые причины:
- корневой элемент документа;
- позиционированный элемент с
z-index, отличным отauto; position: fixedилиsticky;- flex- или grid-элемент с
z-index, отличным отauto; opacityменьше1;transformилиfilter, отличные отnone;isolation: isolate;- некоторые значения
containиwill-change.
Даже невидимая трансформация создаёт новый контекст:
.card {
transform: translateZ(0);
}Предсказуемая система слоёв
Вместо случайно растущих чисел используйте ограниченную шкалу:
:root {
--layer-content: 0;
--layer-dropdown: 10;
--layer-sticky: 20;
--layer-overlay: 30;
--layer-modal: 40;
--layer-notification: 50;
}Отдельный контекст приложения можно создать без визуальных эффектов:
.app {
isolation: isolate;
}Модальное окно
.modal {
position: fixed;
z-index: var(--layer-modal);
inset: 0;
display: grid;
place-items: center;
padding: 1rem;
background: rgb(0 0 0 / 55%);
}
.modal__content {
width: min(100%, 36rem);
max-height: calc(100dvh - 2rem);
overflow: auto;
background: white;
border-radius: 1rem;
}Полноценному модальному окну также требуется управление фокусом и закрытие с клавиатуры средствами JavaScript.
Отладка z-index
Если z-index не работает:
- найдите ближайших предков сравниваемых элементов;
- проверьте
position,z-index,opacity,transform,filter,isolationиcontain; - определите, какие предки создают stacking context;
- сравните уровни родительских контекстов;
- удалите лишний контекст или переместите всплывающий элемент в подходящий верхнеуровневый контейнер.
overflow: hidden не создаёт stacking context само по себе, но может обрезать меню или подсказку.
Адаптивная типографика
Базовый размер
html {
font-size: 100%;
}
body {
font-size: 1rem;
line-height: 1.5;
}Для текста и интерфейсных размеров удобно использовать rem, сохраняя пользовательские настройки браузера.
Плавный размер шрифта
h1 {
font-size: clamp(2rem, 1.2rem + 3vw, 4.5rem);
line-height: 1.05;
}Размер плавно изменяется, но остаётся в заданном диапазоне.
Шкала размеров:
:root {
--font-body: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
--font-h3: clamp(1.25rem, 1.1rem + 0.7vw, 1.75rem);
--font-h2: clamp(1.5rem, 1.2rem + 1.5vw, 2.5rem);
--font-h1: clamp(2rem, 1.4rem + 3vw, 4rem);
}Длина строки
.article {
max-width: 65ch;
}
.lead {
max-width: 50ch;
font-size: 1.125rem;
}ch приблизительно соответствует ширине символа 0, а не точному числу символов.
Длинный текст
.prose {
overflow-wrap: anywhere;
hyphens: auto;
}Для автоматических переносов важно указать язык документа:
<html lang="ru">Балансировка строк заголовка:
h1,
h2,
h3 {
text-wrap: balance;
}Раскладка не должна критически зависеть от text-wrap, поскольку точное поведение может различаться.
Адаптивные изображения
Базовое правило
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
img {
height: auto;
}object-fit и aspect-ratio
.card__image {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
object-position: center top;
}coverзаполняет область с возможной обрезкой;containполностью помещает изображение и может оставить свободное место.
Размеры в HTML
<img
src="photo.jpg"
width="1200"
height="800"
alt="Описание изображения"
>Атрибуты width и height позволяют браузеру заранее вычислить пропорции и зарезервировать место, уменьшая скачки раскладки.
srcset и sizes
<img
src="photo-800.jpg"
srcset="
photo-480.jpg 480w,
photo-800.jpg 800w,
photo-1200.jpg 1200w
"
sizes="
(min-width: 900px) 50vw,
100vw
"
width="1200"
height="800"
alt="Описание изображения"
>srcsetсообщает доступные размеры файлов;sizesописывает ожидаемую ширину изображения в раскладке;- браузер выбирает ресурс с учётом экрана и плотности пикселей.
sizes должно соответствовать реальной CSS-раскладке.
<picture>
Используется для другого кадрирования или формата:
<picture>
<source
media="(min-width: 768px)"
srcset="hero-wide.webp"
type="image/webp"
>
<source srcset="hero-mobile.webp" type="image/webp">
<img
src="hero-mobile.jpg"
width="800"
height="1000"
alt="Описание изображения"
>
</picture>Ленивая загрузка
<img
src="gallery-item.jpg"
width="800"
height="600"
loading="lazy"
decoding="async"
alt="Описание изображения"
>loading="lazy" подходит для изображений ниже первого экрана. Главному изображению первого экрана ленивую загрузку обычно не назначают.
Если изображение несёт смысл, предпочтительнее <img> или <picture>, а не background-image, у которого нет alt.
Общий пример
:root {
--color-primary: #2563eb;
--color-text: #1f2937;
--color-muted: #6b7280;
--color-surface: #fff;
--color-background: #f3f4f6;
--color-border: #e5e7eb;
--container-max: 75rem;
--page-padding: clamp(1rem, 4vw, 2rem);
--section-space: clamp(2rem, 6vw, 5rem);
--layout-gap: clamp(1.5rem, 4vw, 4rem);
--layer-sticky: 20;
--header-height: 4rem;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-padding-top: calc(var(--header-height) + 1rem);
}
body {
margin: 0;
min-width: 20rem;
min-height: 100dvh;
color: var(--color-text);
background: var(--color-background);
font-family: system-ui, sans-serif;
font-size: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
line-height: 1.5;
}
img {
display: block;
max-width: 100%;
}
.container {
width: min(
100% - 2 * var(--page-padding),
var(--container-max)
);
margin-inline: auto;
}
.site-header {
position: sticky;
z-index: var(--layer-sticky);
top: 0;
min-height: var(--header-height);
background: rgb(255 255 255 / 90%);
border-bottom: 1px solid var(--color-border);
backdrop-filter: blur(12px);
}
.site-header__inner {
display: flex;
min-height: var(--header-height);
flex-wrap: wrap;
align-items: center;
gap: 0.75rem 1.5rem;
}
.navigation {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1.25rem;
margin-inline-start: auto;
}
.layout {
display: grid;
grid-template-columns: 1fr;
gap: var(--layout-gap);
padding-block: var(--section-space);
}
.content {
min-width: 0;
}
.cards {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(100%, 15rem), 1fr)
);
gap: clamp(1rem, 2vw, 1.5rem);
}
.card {
display: flex;
min-width: 0;
overflow: hidden;
flex-direction: column;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 1rem;
}
.card__image {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}
.card__content {
display: flex;
flex: 1;
flex-direction: column;
padding: 1.25rem;
}
.card__title {
margin: 0;
font-size: clamp(1.25rem, 1.1rem + 0.5vw, 1.5rem);
line-height: 1.2;
text-wrap: balance;
}
.card__description {
color: var(--color-muted);
overflow-wrap: anywhere;
}
.card__button {
margin-top: auto;
}
@media (min-width: 60rem) {
.layout {
grid-template-columns: 16rem minmax(0, 1fr);
align-items: start;
}
.sidebar__inner {
position: sticky;
top: calc(var(--header-height) + 1.5rem);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}Частые ошибки
Фиксированная ширина
/* Может вызвать горизонтальную прокрутку */
.card {
width: 600px;
}
/* Адаптивный вариант */
.card {
width: min(100%, 600px);
}Проценты вместе с gap
Две колонки по 50% и промежуток могут быть шире контейнера. Используйте flex: 1 1 0 или Grid с minmax(0, 1fr).
Отсутствие min-width: 0
Длинное содержимое может расширить гибкую колонку:
.layout {
grid-template-columns: 240px minmax(0, 1fr);
}
.content {
min-width: 0;
}Слишком много брейкпоинтов
Не создавайте медиазапрос для каждой модели устройства. Добавляйте брейкпоинт, когда содержимое становится неудобным.
Изменение смыслового порядка только CSS
order, обратные направления Flexbox и grid-области меняют визуальное расположение, но не обязательно порядок чтения и фокуса. Логический порядок задаётся в HTML.
Бесконечное увеличение z-index
Проблема часто находится в родительском stacking context, а не в маленьком числе.
100vw для обычного блока
100vw может учитывать ширину вертикальной полосы прокрутки. Для обычного блока страницы чаще достаточно width: 100%.
Фиксированная высота текста
Текст может переполнить блок. Обычно безопаснее min-height или высота по содержимому.
Краткая памятка
Flexbox:
.container {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.item {
flex: 1 1 15rem;
min-width: 0;
}Grid:
.grid {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(100%, 15rem), 1fr)
);
gap: 1.5rem;
}Контейнер:
.container {
width: min(100% - 2rem, 75rem);
margin-inline: auto;
}Адаптивный размер:
.element {
padding: clamp(1rem, 4vw, 3rem);
font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
}Sticky-элемент:
.element {
position: sticky;
top: 1rem;
}Адаптивное изображение:
.image {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}Итоги
- Flexbox решает преимущественно одномерные задачи.
- Grid создаёт двумерные сетки и общую структуру страницы.
gapзадаёт предсказуемые промежутки между элементами.minmax(0, 1fr)иmin-width: 0предотвращают частые переполнения.auto-fitвместе сminmax()создаёт адаптивные сетки без множества медиазапросов.- Mobile-first начинает с узкой версии и расширяет её через
min-width. %, viewport-единицы,clamp(),min(),max()иcalc()создают гибкие размеры.- CSS-переменные поддерживают единую систему размеров и слоёв.
z-indexдействует внутри stacking context.- Адаптивные шрифты должны иметь разумные минимальные и максимальные значения.
- Изображения должны сохранять пропорции, резервировать место и загружаться в подходящем размере.
- Брейкпоинты выбирают по поведению содержимого, а не по названиям устройств.