"Умные" макеты с контейнерными запросами

Источник: «“Smart” Layouts With Container Queries»
Современный CSS даёт множество новых, простых способов решения старых проблем, но часто новые функции не только решают старые проблемы, но и открывают новые возможности.

Контейнерные запросы — одна из тех вещей, которые открывают новые возможности, но поскольку они очень похожи на старый способ работы с медиа-запросами, наш первый инстинкт — использовать их так же, или, по крайней мере, очень похоже.

Но при этом мы не используем преимущества того, насколько умными являются контейнерные запросы по сравнению с медиа-запросами!

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

Давайте на простом примере проиллюстрируем, что именно имеется в виду:

html {
font-size: 32px;
}

body {
background: lightsalmon;
}

@media (min-width: 35rem) {
body {
background: lightseagreen;
}
}

Каким должен быть размер области просмотра, чтобы цвет фона изменился? Если скажете, что ширина 1120px — а это результат умножения 35 на 32 для тех, кто не потрудился посчитать — вы не одиноки в этом предположении, но тоже будете неправы.

Помните, я говорил, что медиа запросы знают не так уж много? Есть только две вещи, которые они знают:

И когда я говорю о размере шрифта браузера, я не имею в виду размер корневого шрифта в документе, поэтому 1120px в приведённом выше примере было неверным.

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

И да, это сделано специально. Спецификация медиа-запроса гласит:

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

Это может показаться странным решением, но если бы это не сработало, что бы произошло, если сделать вот так:

html {
font-size: 16px;
}

@media (min-width: 30rem) {
html {
font-size: 32px;
}
}

Если бы медиа-запрос смотрел на размер корневого font-size (как предполагает большинство), то столкнулись бы с циклом, когда область просмотра достигла бы ширины 480px, где font-size увеличивался бы в размере, а затем снова и снова уменьшался.

Контейнерные запросы намного умнее

В то время как у медиа-запросов есть это ограничение, и не зря, контейнерным запросам не нужно беспокоиться о подобной проблеме, и это открывает множество интересных возможностей!

Например, допустим, есть grid, который должен складываться в стопку при малых размерах, но занимать три колонки при больших размерах. С помощью медиа-запросов приходится использовать магические числа (Magic Numbers in CSS), чтобы точно определить, где это должно произойти. Используя контейнерный запрос, можно определить минимальный размер столбца, и он всегда будет работать, потому что используется размер контейнера.

Это означает, что не нужно магическое число для точки разрыва. Если нужны три колонки с минимальным размером 300px, можно получить три колонки при ширине контейнера 900px. Если делать это с помощью медиа-запроса, это не сработает, потому что при ширине области просмотра 900px контейнер чаще всего меньше этого размера.

Но ещё лучше то, что можно использовать любые единицы измерения, потому что контейнерные запросы, в отличие от медиа-запросов, могут смотреть на размер шрифта самого контейнера.

На мой взгляд, ch идеально подходит для таких вещей. Используя ch, можно сказать: Когда будет достаточно места для каждого столбца шириной не менее 30 символов, нужно сделать три столбца.

Можно посчитать вот так:

.grid-parent { container-type: inline-size; }

.grid {
display: grid;
gap: 1rem;

@container (width > 90ch) {
grid-template-columns: repeat(3, 1fr);
}
}

И это действительно работает, как видно из этого примера.

See the Pen

В качестве ещё одного бонуса, благодаря Мириам Сюзанне, я недавно узнал, что можно включать calc() в медиа-запросы и контейнерные запросы, так что вместо того, чтобы вычислять всё самостоятельно, можно включить его так: @container (width > calc(30ch * 3)), как показано в этом примере:

See the Pen

Более практичный вариант использования

Один из раздражающих моментов в использовании контейнерных запросов — это необходимость иметь определённый контейнер. Контейнер не может запрашивать сам себя, поэтому нужна дополнительная обёртка над элементом, который необходимо выбрать с помощью контейнерного запроса. В примерах выше видно, что для работы этого запроса нужен контейнер снаружи grid.

Ещё больше раздражает, когда нужно, чтобы дочерние элементы grid или flex меняли свой макет в зависимости от того, сколько места у них есть, только тогда, чтобы понять, что это не работает, если родитель является контейнером. Вместо того чтобы использовать контейнер grid или flex в качестве определённого контейнера, приходится оборачивать каждый элемент grid или flex в такой контейнер:

<div class="grid">
<div class="card-container">
<div class="card">
</div>
<div class="card-container">
<div class="card">
</div>
<div class="card-container">
<div class="card">
</div>
</div>
.card-container { container-type: inline-size; }

Это не так уж плохо, но немного раздражает.

Но есть способы это обойти

Например, если вы используете repeat(auto-fit, ...), можно использовать основной grid в качестве контейнера!

.grid-auto-fit {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(min(30ch, 100%)), 1fr);
container-type: inline-size;
}

Зная, что минимальный размер колонки составляет 30ch, можно использовать эту информацию для изменения стиля отдельных элементов сетки в зависимости от того, сколько колонок есть:

/* 2 колонки + зазор */
@container (width > calc(30ch * 2 + 1rem)) { ... }

/* 3 колонки + зазор */
@container (width > calc(30ch * 3 + 2rem)) { ... }

Я использовал это в данном примере, чтобы изменить стили первого дочернего элемента в grid в зависимости от того, один, два или три столбца имеется.

See the Pen

И хотя изменение цвета фона отлично подходит для демонстрации, конечно, можно сделать гораздо больше:

See the Pen

Недостатки этого подхода

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

Со временем ситуация должна измениться, поскольку пользовательские медиа-запросы находятся в черновике спецификации Media Queries Level 5, но они уже давно находятся там без каких-либо изменений со стороны браузеров, так что может пройти немало времени, прежде чем их можно будет использовать.

И хотя считаю, что наличие пользовательских свойств позволило бы сделать их более читаемыми и более простыми в обновлении, это открывает достаточно возможностей, чтобы обойтись без них.

Что насчёт flexbox

В flexbox элементы flex определяют макет, поэтому немного странно, что размеры, которые применяются к элементам, важны в точках разрыва.

Это всё ещё может работать, но есть большая проблема, которая может возникнуть, если сделать это с помощью flexbox. Прежде чем рассмотреть эту проблему, приведу небольшой пример того, как можно заставить это работать с flexbox:

.flex-container {
display: flex;
gap: 1rem;
flex-wrap: wrap;

container-type: inline-size;
}

.flex-container > * {
/* Во всю ширину при небольших размерах */
flex-basis: 100%;
flex-grow: 1;

/* когда есть место для 3 колонок, включая зазор */
@container (width > calc(200px * 3 + 2rem)) {
flex-basis: calc(200px);
}
}

В данном случае я воспользовался px, чтобы показать, что это тоже работает, но можно использовать любые единицы измерения, как это было в примерах с grid.

Это может выглядеть как то, в чем вы можете использовать медиа-запрос — в них тоже можно использовать calc()! — Но это будет работать только в том случае, если ширина родительского элемента совпадает с шириной области просмотра, что чаще всего не так.

See the Pen

Это не работает, если у flex элементов есть padding

Многие люди не понимают этого, но алгоритм flexbox не учитывает отступы и границы, даже если изменить box-sizing. Если есть отступы на элементах flexbox, придётся прибегнуть к магическому числу, чтобы заставить его работать.

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

See the Pen

Из-за этого я чаще использую подход с grid, чем с flexbox, но есть ситуации, когда он может сработать.

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

See the Pen

Открываются интересные возможности

Начав играть с подобными вещами, я обнаружил, что они открывают новые возможности, которых не было при использовании медиа-запросов, и это заставляет с нетерпением ждать, что ещё возможно!

Дополнительные материалы

Предыдущая Статья

Всё о циклах в JavaScript

Следующая Статья

Десять редко используемых правил валидации Laravel