Макеты CSS и адаптивный дизайн

  1. 1. Основы
    1. Какой режим макета использовать?
    2. Принципы работы
  2. 2. Flexbox
  3. 3. Сетка и подсетка
    1. Пример кода: сетка и подсетка
  4. 4. Запросы контейнера
    1. Пример кода: адаптивная типографика с использованием единиц запроса контейнера
  5. 5. Нативные оверлеи, позиционирование фиксированных объявлений и контексты наложения
  6. 6. Отслеживание переполнения и стабильность макета
  7. 7. Механика области просмотра и распространение треков
  8. 8. Сетка (masonry)

1. Основы

По возможности используйте движок макета браузера для повышения производительности. Используйте внутренние размеры, логические свойства и aspect-ratio, прежде чем прибегать к жестко заданным размерам или сложным запросам медиа.

1.1. Какой режим макета использовать?

Пройдите по дереву решений сверху вниз и остановитесь на первом совпадении. Обратите внимание, что макеты могут быть вложены друг в друга, и каждое решение основано на варианте использования этого контейнера.

  1. Это простая строка ИЛИ столбец элементов? Используйте flexbox – одномерный макет, в котором контент распределяется по одной оси.
  2. Должен ли вложенный элемент выравниваться по дорожкам сетки своего родительского элемента? Используйте subgrid – двумерную сетку, в которой на первом месте стоят отношения между элементами. Она наследует родительские дорожки, поэтому дочерние элементы могут выравниваться по соседним элементам.
  3. Имеет ли страница или компонент сложную структуру со строками И столбцами? Используйте grid – двухмерный макет, в котором вы определяете структуру, а контент заполняет ее.
  4. Представляет ли контент собой длинный текст, который нужно разбить на сбалансированные столбцы? Используйте несколько столбцов – одномерный поток, как в газете.
  5. Нужно ли плотно упаковать предметы разной высоты? Используйте grid с grid-auto-flow: dense уже сегодня. Переходите на встроенную сетку masonry (также известную как grid lanes), только когда она станет доступна в вашей целевой версии Baseline (см. раздел 8).
  6. Нужно ли, чтобы элемент плавал над страницей и оставался пространственно привязанным к триггеру, даже за пределами границ DOM или контекстов наложения? Используйте позиционирование привязки – anchor-name для триггера, position-anchor для оверлея (см. §5).

1.2. Принципы работы

  • Используйте логические свойства (inline-size, block-size, margin-inline, padding-block, inset-inline-start) для размеров макета и интервалов. Подробнее об этом рассказывается в статье CSS: современная архитектура.
  • Применяйте ментальную модель "сначала контент" или "сначала макет": flexbox, когда элементы диктуют поток, и grid, когда вы сначала определяете скелет.
  • Используйте сокращения place-* (place-content, place-items, place-self), чтобы выровнять элементы по обеим осям в одном объявлении.
  • Используйте внутренние размеры (min-content, max-content, fit-content()) и гибкие дорожки (fr, minmax()) вместо фиксированных width/height. Это позволит уменьшить количество медиазапросов и сделать макеты более устойчивыми.
  • НЕ используйте overflow: hidden, чтобы скрыть проблемы с макетом. Устраняйте их в источнике. Например, некоторые заменяемые элементы, такие как <canvas>, <img>, <svg>, <video>, <iframe>, по умолчанию используют display: inline, что может привести к появлению пробела в некоторых макетах. Вместо того чтобы обрезать пробел с помощью overflow, удалите его, применив значение display-outside (например, display: block, grid, flex и т.д.).
  • Используйте aspect-ratio, чтобы зарезервировать место для медиаконтента и предотвратить смещение макета до загрузки объектов.
.sidebar       { inline-size: max-content; }    /* Size to longest unbreakable token. */
.main-content  { inline-size: fit-content; }    /* Grow to available space, no further. */
.media         { aspect-ratio: 16 / 9; inline-size: 100%; block-size: auto; }
body.centered  { display: grid; place-content: center; min-block-size: 100dvb; }

Информацию о calc-size() и внутреннем размере с учетом ограничений можно найти в разделе Расчет с использованием внутренних размеров.

2 Flexbox

Одномерный макет – элементы располагаются вдоль одной главной оси и выравниваются по поперечной оси. Используйте его для панелей навигации, панелей инструментов, строк элементов и любого распределения по одной строке или столбцу.

Что рекомендуется делать

  • Задайте контекст с помощью display: flex и установите главную ось с помощью flex-direction (по умолчанию row).
  • Используйте flex-wrap: wrap, когда возможно переполнение. nowrap без overflow: auto/hidden будет выходить за пределы узких областей просмотра.
  • Добавьте flex-wrap: balance после flex-wrap: wrap в строках с переносом компактных элементов (например, облаков тегов, фильтров, групп кнопок или ссылок навигации), чтобы равномерно распределить элементы по строкам и избежать появления одного элемента в последней строке. В браузерах без поддержки будет использоваться wrap.
  • Используйте сокращенное свойство flex <grow> <shrink> <basis> (например, flex: 1 1 250px) для элементов вместо того, чтобы задавать flex-grow/flex-shrink/flex-basis по отдельности.
  • Используйте gap (или row-gap/column-gap) для интервалов между элементами вместо дочерних полей.
  • Добавьте префикс safe к выравниванию по позиции (например, align-items: safe center), чтобы контент, на который можно установить фокус, не обрезался, когда контейнер уже, чем контент.
  • Переместите один элемент в дальний конец главной оси с помощью margin-inline-start: auto (или margin-block-start: auto) – это стандартный способ.
  • Переопределить выравнивание по поперечной оси для каждого элемента с помощью align-self.
  • Используйте align-items, чтобы выровнять все элементы по центру поперечной оси. Используйте margin: auto для одного элемента, чтобы выровнять его по центру обеих осей независимо. Используйте align-content, только если контейнер переносит элементы и имеет дополнительное пространство между строками.
  • Задайте min-inline-size: 0 (или min-width: 0) для элементов flex, содержащих длинный неразрывный контент (URL, код, длинные строки). По умолчанию элементы flex не сжимаются до размера меньше размера их контента, что приводит к переполнению.

Не делайте следующее:

  • Не используйте justify-self для гибких элементов – это свойство работает только для макетов с сеткой, блоками и абсолютным позиционированием. Используйте автоматические поля.
  • Не используйте order или flex-direction: *-reverse, чтобы изменить порядок интерактивного контента. Они меняют только визуальный порядок. Порядок DOM по-прежнему определяет последовательный фокус, поэтому поток вкладок клавиатуры не будет соответствовать тому, что видит пользователь.
  • Не путайте space-around (половинные отступы по краям) с space-evenly (одинаковые отступы до, между и после).
  • Не забывайте о смене осей: если flex-direction: column, то justify-content выравнивает по блочной оси, а align-items – по строчной оси (противоположно значению по умолчанию).
  • Не задавайте размеры контейнера и его дочерних элементов так, чтобы они заполняли друг друга. Это распространенная причина переполнения и неожиданных результатов. Укажите точный размер одной стороны.
  • Не задавайте для одного элемента и flex-basis, и width/inline-size. В контексте flex приоритет имеет flex-basis, а width игнорируется. Используйте flex-basis (или сокращение flex) в качестве единого источника достоверных данных для определения размеров гибких элементов.
.card-grid        { display: flex; flex-flow: row wrap; gap: 1rem; }
.card-item        { flex: 1 1 250px; }                  /* grow, shrink, basis */
.card-item-action { margin-inline-start: auto; }        /* Push to main-axis end. */
.toolbar          { display: flex; align-items: safe center; }
.tag-list         { display: flex; flex-wrap: wrap; flex-wrap: balance; gap: 0.5rem; }

3. Сетка и подсетка

Двухмерный макет – строки и столбцы задаются явно или определяются движком. Subgrid позволяет вложенной сетке наследовать дорожки родительской сетки, чтобы внуки выравнивались по братьям и сестрам.

Как выбрать функции сетки

  • Вы точно знаете, сколько столбцов вам нужно?
    • Да – использовать треки с контентом для взрослых (grid-template-columns: 200px 1fr, repeat(3, 1fr) и т. д.)
    • Нужны ли разные размеры для разных столбцов (боковая панель + основная, заголовок, охватывающий все)? → используйте grid-template-areas для именованных, читаемых регионов
    • Все ли столбцы одинаковы или они расположены только по номеру строки? → используйте repeat(N, ...) или именованные линии
    • Нет (адаптивный дизайн, количество товаров неизвестно) – используйте repeat(auto-fit, minmax(min, 1fr)).
    • Должны ли элементы в последней строке растягиваться, чтобы заполнить оставшееся пространство? → auto-fit
    • Должны ли пустые дорожки последней строки сохранять свой минимальный размер (сохраняя фантомные слоты столбцов)? → auto-fill
  • Вам нужно разместить объект в определенном месте?
    • Да – используйте grid-column: <start> / <end> или grid-area: <name>.
    • Нет (просто охватывает несколько дорожек, положение в потоке не имеет значения) – используйте grid-column: span <n>.
  • Должны ли дочерние элементы наследовать размеры дорожек родительской сетки (выравнивание по неровным краям между дочерними элементами)?
    • Да – использовать вложенную сетку на затронутой оси.
    • Количество дочерних элементов в ячейке может быть разным? → subgrid только для одной оси; для другой используйте grid-auto-rows/grid-auto-columns
    • Количество детей фиксировано? → subgrid on both axes is fine
    • Нет – стандартная сетка, дополнительная сетка не требуется.

Что рекомендуется делать

  • Установите контекст с помощью display: grid.
  • Используйте grid-template-areas для сложных макетов на уровне страницы – названия областей говорят сами за себя, а объявление можно выровнять по строкам и столбцам для удобства чтения.
  • Используйте repeat(auto-fit, minmax(200px, 1fr)) для адаптивных сеток карточек, которые растягивают заполненные дорожки, чтобы заполнить строку, или auto-fill, чтобы сохранить пустые повторяющиеся дорожки минимального размера.
  • Используйте fr для пропорционального распределения дорожек и minmax(min, max) для гибких, но ограниченных дорожек.
  • Размещайте элементы с помощью grid-column: span <n>, чтобы задать размер по дорожкам, grid-column: <start> / <end>, чтобы разместить на определенных строках, или grid-area: <name> для именованных регионов.
  • Используйте вложенную сетку (grid-template-columns: subgrid или grid-template-rows: subgrid), чтобы решить проблему неровных краев в списках карточек. Внутренние элементы, такие как заголовки, метаданные и призывы к действию, выравниваются по соседним элементам.
  • Чтобы обеспечить совместимость со старыми браузерами, добавьте объявление subgrid после объявления grid-template-rows/-columns.

Не делайте следующее:

  • Не ожидайте, что размер дорожки auto-fit/auto-fill будет определяться содержимым элемента. Он определяется аргументом размера repeat().
  • Не используйте grid-auto-flow: dense для интерактивного контента. Он эффективно упаковывает элементы, но визуально меняет их порядок, нарушая порядок перехода по вкладкам клавиатуры в DOM.
  • Не применяйте субсетку к обеим осям, если количество дочерних элементов может меняться. Дополнительные элементы попадают в последнюю дорожку. Вместо этого используйте grid-auto-rows/grid-auto-columns для неявной оси.
  • Не путайте justify-items/align-items (выравнивает контент элемента внутри его полосы) с justify-content/align-content (выравнивает полосы сетки внутри контейнера). Если вы укажете неверный, он просто не будет работать.
  • Не используйте repeat(auto-fit/auto-fill, ...) без определенного inline-size в контейнере. Внутри display: inline-grid или flex-элемента без размера у контейнера нет ширины, которую можно разделить, поэтому количество дорожек будет непредсказуемым.

3.1. Пример кода: сетка и подсетка

Оболочка страницы: <main class="page-layout"> содержит <header>, <aside>, <section class="card-grid"> с <div class="card"> дочерними элементами и <footer>.

/* Align grid-template-areas in rows and columns for readability. */
.page-layout {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-areas:
    "header  header  header"
    "sidebar main    main"
    "footer  footer  footer";
  gap: 1.5rem;
}

header  { grid-area: header; }
aside   { grid-area: sidebar; }
footer  { grid-area: footer; }

.card-grid {
  grid-area: main;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  grid-template-rows: auto 1fr; /* title block, body block */
  gap: 1rem;
}

.card {
  grid-row: span 2;
  display: grid;
  /* Same-cascade fallback: ignored when subgrid is supported. */
  grid-template-rows: auto 1fr;
  grid-template-rows: subgrid;
}

4 запроса контейнера

Запрашивать размер (или вычисленный стиль) родительского контейнера, а не области просмотра. Ментальная модель: запросы контейнера = контекст компонента; медиа-запросы = глобальный макет страницы и пользовательские настройки (prefers-color-scheme, prefers-reduced-motion).

Что рекомендуется делать

  • Прежде чем запрашивать дочерние элементы, установите контекст контейнера с помощью container-type: inline-size (запросы только по ширине) или container-type: size (обе оси) в оболочке.
  • Если вложенные контексты могут конфликтовать, присвойте контейнерам имена с помощью container-name (или сокращенного варианта container: container: card / inline-size).
  • Включать единицы запросов контейнера в расчет адаптивного типа и интервалов: cqi/cqb (логические встроенные/блочные), cqw/cqh (физические), cqmin/cqmax.
  • Если используется container-type: size, задайте для контейнера определенное значение block-size. В противном случае дочерние элементы будут свернуты, поскольку ограничение размера заставляет контейнер игнорировать свой контент.

Не делайте следующее:

  • Не используйте block-size в качестве значения container-type – это недопустимо. Используйте size для обеих осей.
  • Не ожидайте, что внутренний размер дочерних элементов повлияет на контейнер после объявления container-type. Контейнер вычисляется так, как будто у него нет дочерних элементов, когда ограничение активно.
  • Не полагайтесь на единицы измерения запросов контейнера внутри потомков неподходящего предка; они возвращаются к малому окну просмотра (svw/svh).

4.1. Пример кода: адаптивная типографика с использованием единиц измерения запросов контейнера

.card-wrapper {
  container: card / inline-size; /* shorthand for container-name / container-type */
}

@container card (inline-size > 400px) {
  .content {
    display: flex;
    gap: 2rem;
  }
}

.title {
  /* Fluid type bound to the container width, not the viewport. */
  font-size: clamp(1rem, 4cqi, 2rem);
}

О том, как создавать адаптивные стили на основе компонентов, читайте в статьях Стили, зависящие от размера и Гибкое масштабирование.

5. Нативные оверлеи, позиционирование привязки и контексты наложения

Когда использовать каждый примитив наложения:

  • Используйте popover для временных немодальных элементов интерфейса (всплывающих окон, уведомлений, подсказок). Они находятся на верхнем уровне, поэтому управлять z-index не нужно.
  • Используйте <dialog> с .showModal() для модальных взаимодействий, требующих захвата фокуса и инертного фона.
  • Не используйте popover и .showModal() для одного элемента, так как они являются взаимоисключающими состояниями времени выполнения.

Позиционирование якоря (пространственное расположение оверлеев):

  • Используйте position-area (или anchor() для вставок) и anchor-size(), чтобы задать положение и размер оверлея относительно триггера.
  • Используйте position-try-fallbacks: flip-block (или flip-inline), чтобы браузер мог изменить положение, когда оверлей выходит за пределы области просмотра.
  • Не смешивайте физические и логические ключевые слова в одном значении position-area – выберите одну систему координат.
  • Используйте @supports (anchor-name: --x) для определения поддержки функции и укажите резервное значение absolute для свойства position.

Полные сведения о реализации, стратегии полифилов и справочные данные о значении popover можно найти в статьях Декларативное управление диалоговыми окнами и всплывающими элементами и Подсказки с учетом положения. Примеры позиционирования с привязкой для раскрывающихся списков, всплывающих окон и индикаторов вкладок можно найти в статьях Раскрывающиеся списки с учетом области просмотра и Подчеркивание вкладок с позиционированием с привязкой.

6. Отслеживание переполнения и стабильность макета

Управляйте смещением макета, полосами прокрутки и обрезкой.

Что рекомендуется делать

  • Используйте overflow: auto, чтобы полосы прокрутки появлялись только тогда, когда контент действительно выходит за пределы контейнера.
  • Используйте overflow: clip, чтобы обрезать контент без создания контейнера прокрутки. Чтобы разрешить выход за пределы контейнера, используйте overflow-clip-margin.
  • Используйте свойство scrollbar-gutter: stable, чтобы зарезервировать место для полос прокрутки и предотвратить смещение макета при увеличении объема контента.
  • Используйте overscroll-behavior: contain (или none) для вложенных областей прокрутки (например, боковых панелей, модальных окон и списков), чтобы цепочки прокрутки не переходили на страницу, или overscroll-behavior: chain для промежуточных оболочек прокрутки (например, дорожек, открывающихся свайпом), когда избыточная прокрутка должна распространяться на родительский скроллер без запуска локального отскока резиновой ленты.
  • Для усечения многострочного текста используйте триаду -webkit-line-clamp + display: -webkit-box + -webkit-box-orient: vertical. Несмотря на префикс, этот шаблон полностью определен и не устарел. Объявите сокращенное свойство line-clamp без префикса. Браузеры, которые его не поддерживают, просто проигнорируют его. Не делайте следующее:

  • Не используйте overflow: scroll, если можно использовать auto. scroll принудительно добавляет полосы прокрутки, даже если прокручивать нечего.

  • Не используйте overflow: hidden, если вам нужно только обрезать контент. hidden создает контейнер прокрутки, который можно прокручивать программно.

.scrollable-list {
  max-block-size: 400px;
  overflow-y: auto;
  scrollbar-gutter: stable;       /* Reserve scrollbar space. */
  overscroll-behavior: contain;   /* No scroll chaining into the page. */
}

.snippet {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  line-clamp: 3;                  /* Ignored where unsupported. */
  overflow: clip;
}

Подробные сведения о overflow: clip и overflow-clip-margin приведены в статье Управление обрезкой переполнения. О том, как передавать вложенную прокрутку в overscroll-behavior: chain в интерфейсе с жестом смахивания, читайте в статье Жест смахивания. Чтобы узнать, как настроить цвет, размер и тему полосы прокрутки, ознакомьтесь со статьями Как настроить цвет или толщину полосы прокрутки, Тёмная тема и Как настроить полосу прокрутки для режима высокой контрастности.

7. Механика области просмотра и распределение дорожек

  • Используйте dvh/dvw для контейнеров макетов на мобильных устройствах, которые должны учитывать сдвиг интерфейса браузера (сворачивание/разворачивание панели URL).
  • Не используйте 100vw для макета на всю ширину экрана, так как при этом не учитывается ширина полосы прокрутки и возникает горизонтальное переполнение. Используйте вместо него 100%, 100dvw или 100svw.

О том, как начать работу с адаптивным дизайном (динамические единицы области просмотра, резервирование места для медиаконтента, гибкая типографика), читайте в статье Адаптивный дизайн.

8 дорожек сетки (мозаика)

Спецификация находится в разработке под названием grid lanes (например, display: grid-lanes), а в более ранних экспериментальных прототипах использовалось grid-template-rows: masonry.

Что рекомендуется делать

  • Используйте сетку с grid-auto-flow: dense для плотной упаковки, учитывая, что порядок DOM может не совпадать с визуальным порядком.
  • Используйте многоколоночный макет (columns: 3; column-gap: 1rem) для контента, похожего на кирпичную кладку, когда элементы представляют собой фрагменты документа, а не карточки равного веса.
  • Используйте grid-template-rows: masonry только для прогрессивного улучшения. Для определения поддержки используйте @supports.

Не делайте следующее:

  • Не делайте grid-template-rows: masonry обязательным требованием, пока целевой показатель Baseline не достигнет нужного уровня.
.gallery       { columns: 3 200px; column-gap: 1rem; }
.gallery > *   { break-inside: avoid; margin-block-end: 1rem; }

@supports (grid-template-rows: masonry) {
  .gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    grid-template-rows: masonry;
    gap: 1rem;
    columns: unset;
  }
}