CSS Layout — Flexbox, Grid и адаптивная вёрстка

CSS Layout — набор механизмов CSS для размещения элементов, распределения свободного пространства и адаптации интерфейса к разным размерам экрана.

Основные инструменты:

Flexbox и Grid дополняют друг друга. Grid обычно используют для общей структуры страницы и сеток, а Flexbox — для расположения элементов внутри компонентов.

Содержание


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-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;
  }
}

Выбор брейкпоинтов

Брейкпоинты выбирают по поведению содержимого, а не по конкретным моделям устройств:

  1. начать с узкой версии;
  2. постепенно увеличивать ширину окна;
  3. найти момент, когда содержимое становится неудобным;
  4. добавить брейкпоинт, исправляющий раскладку.
/* Базовые стили */

@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).

.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-паттерны

Закреплённая шапка остаётся у верхней границы во время прокрутки:

.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:

Если шапка перекрывает заголовок после перехода по якорю:

html {
  scroll-padding-top: 80px;
}

.section {
  scroll-margin-top: 80px;
}

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

Частые причины:

Даже невидимая трансформация создаёт новый контекст:

.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 не работает:

  1. найдите ближайших предков сравниваемых элементов;
  2. проверьте position, z-index, opacity, transform, filter, isolation и contain;
  3. определите, какие предки создают stacking context;
  4. сравните уровни родительских контекстов;
  5. удалите лишний контекст или переместите всплывающий элемент в подходящий верхнеуровневый контейнер.

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;
}

Размеры в 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="Описание изображения"
>

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;
}

Итоги