Перевод «Flexible layouts without media queries» Dannie Vinther



С момента появления в браузерах в 2017 году, CSS Grid дал веб-дизайнерам и разработчикам новую суперсилу. На данный момент существует множество статей / руководств, иллюстрирующий возможности и преимущества CSS Grid, описывающих всё – от вдохновлённых ASCII-синтаксисом способом разметки Grid-областей до автоматического размещения элементов, делающих медиа-запросы чем-то устаревшим. Тем не менее, медиа-запросы всё ещё играют важную роль и это не может не вызывать некоторые сложности – наверное.


Проблема с медиа-запросами


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


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


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


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


Математические функции


Если мы пролистываем вниз страницу модуля CSS Values and Units Module Level 4 спецификации, то встречаем раздел, называемый "Mathematical Expressions" (математические выражения). Помимо старой доброй функции "calc()" можно найти математические функции min(), max() и clamp(), которые позволяют нам прямо в CSS выполнять более сложные вычисления, чем с использованием только функции calc().


Путаница с именами


Выглядит многообещающе, но поначалу названия этих новых функций могут немного сбивать с толку. Иногда путаница заключается в том, что функция max() используется в значениях свойств, определяющих минимальные параметры (например, когда в свойстве типа min-width используется функция max()) и наоборот. В подобных ситуациях, из-за изобилия в коде min и max можно спутать, какую именно функцию нужно использовать в данном случае.


Теперь давайте рассмотрим некоторые примеры.


Функции min() и max() принимают два значения. Наименьшее и\или наибольшее значения соответственно, разделённые запятой. Рассмотрим следующим пример с использованием min():


width: min(100%, 200px);

Здесь мы говорим, что ширина по умолчанию равна 200px, но она не должна быть больше, чем 100% родительского контейнера. Это, по сути, то же самое, что задать:


width: 100%;
max-width: 200px;

Довольно гибко, правда? А вот пример с использованием max():


width: max(20vw, 200px);

Данное выражение устанавливает ширину элемента равной 20vw, но не позволяет ей становиться меньше 200px.


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


Базовый пример


Когда мы хотим, чтобы коллекция элементов вела себя отзывчиво без явно указанных с помощью медиа-запросов правил, алгоритм автоматического расположения CSS Grid помогает нам сделать это без проведения сложных вычислений. Используя auto-fit или auto-fill вместе с оператором minmax(), мы можем легко сообщить браузеру о необходимости выяснить, когда стоит изменить количество столбцов, таким образом, создавая динамически реагирующую сетку:


grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));


Здесь мы указываем, что размер каждой карточки должен быть минимум 350px, максимум 1fr (единица измерения фракции/части доступного пространства). При использовании auto-fit, каждая карточка может быть больше 350px и мы говорим браузеру втиснуть в контейнер сетки как можно больше карточек одинаковой ширины и гибкого размера.


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



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


Min() или Max()?


В рассматриваемом случае уместно использовать min()


grid-template-columns: repeat(auto-fit, minmax(min(100%, 350px), 1fr));

Мы сообщаем браузеру, что значение должно равняться 350px до тех пор, пока ширина родительского контейнера больше этих 350px. В противном случае берётся значение, равное 100%.




Обратите внимание, что теперь карточки находятся в поле зрения на экранах почти* любого размера.


* "Почти" – потому что контейнер, содержащий карточки, не может стать меньше самого длинного слова.


Получается гораздо лучше. Но можем ли мы сделать еще больше?


Пример с Clamp()


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


Скажем, вместо этого мы хотим две колонки на больших экранах и одну колонку, когда область видимости становится меньше определённой ширины, которую мы выбрали – ни больше ни меньше. Легко, правда? Медиа-запросы… да… Нет!



Как и в предыдущем примере, мы собираемся использовать алгоритм автоматического размещения элементов вместе с математическими функциями, и если мы объединим min() и max(), сможем увидеть реальную мощь математики в CSS. Это даёт нам еще больше контроля над размещением grid-элементов.


Рассмотрим следующее:


min(100%, max(50%, 350px))

Обратите внимание на то, что мы вкладываем max() внутрь min(), чтобы добиться более гибкого значения на выходе. Здесь мы говорим браузеру, что ширина должна быть максимум – 100% ширины контейнера, и минимум 50%, при условии, что 50% больше 350px.


Мы также могли вложить функции наоборот:


max(50%, min(350px, 100%))

Помимо min() и max(), есть ещё функция clamp(), форма записи которой может быть легче для восприятия, поскольку в ней предпочитаемое значение располагается между допустимыми минимумом и максимумом.


clamp(50%, 350px, 100%)

В функции мы указываем, что минимально допустимым значением может быть 50%, предпочитаемое значение – 350px, а максимум – 100%.



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


clamp(50% - 20px, 200px, 100%)

Обратите внимание, что нам не нужно вкладывать функцию calc() внутрь выражения clamp(), если в ней понадобилось выполнить какие-то расчёты. То же касается и функций min() и max().


Можем ли мы пойти еще дальше? Да, можем!


Более математический пример


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



Вы можете подумать, что сделать это будет так же легко, как заменить 50% на, скажем, 33.333% (треть от размера родительского контейнера), создав в конечном счёте три колонки. Если так, боюсь, вы ошибаетесь. При определённой ширине контейнера, достаточно места только для двух карточек, что приводит к образованию двух колонок. Но это не то, что нам нужно.


Расчёты для такой раскладки будут немного сложнее. К счастью, Heydon Pickering уже нашел решение с использванием Flexbox и техники, которую он назвал "Holy Albatross". Он использует преимущества того, как браузер обрабатывает минимальное и максимальное значения. Как сказано в статье Хейдона:


min-width и max-width переопределяют flex-basis. Следовательно, если значение flex-basis абсурдно большое (например, 999rem), значение ширины будет возвращено к значению 100%. Если оно абсурдно маленькое (например, -999rem) по умолчанию значение будет равно 33%

Очень похоже на то, как в CSS Grid работает функция minmax(). Как сказано на MDN:


Если max < min, то max игнорируется и minmax(min, max) обрабатывается как min

В итоге Хейдон пришел к формуле:


calc(40rem - 100% * 999)

Давайте попробуем реализовать эту формулу в нашем Grid-примере. Применение техники "Holy Albatross" даёт нам что-то вроде этого:


minmax(
    clamp(
          33.3333% - var(--gap), /* минимальное значение */
          (40rem - 100%) * 999, /* предпочитаемое значение */
          100% /* максимальное значение */
        ),
        1fr
    )

33.333% обозначает размер каждой из наших карточек и выделяет место под три колонки. Значение не превышает предпочитаемое значение: (40 rem – 100%) * 999. Когда grid-контейнер достигает 40rem, он перестраивается в одну колонку с максимальным значением ширины – 100%.




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


Инструкция для данной разметки гласит: "Если контейнер по ширине становится меньше 40rem, карточки должны перестраиваться".


Если бы мы захотели, чтобы вместо ширины контейнера эта инструкция определялась по предпочтительной ширине отдельных карточек (возможно, со значением, указанным в единицах измерения ch), понадобилось бы внести небольшие изменения в предпочитаемое значение выражения clamp(). Мы делаем это путём умножения минимальной ширины карточек на предпочитаемое количество колонок, что приводит к следующему:


((30ch * 3) - 100%) * 999

Ширина каждой карточки не должна становиться уже 30ch, и если мы еще учтём отступ между карточками, код станет таким:


((30ch * 3 - var(--gap) * 2) - 100%) * 999


Обратите внимание: в приведённом выше Codepen я использовал пользовательские свойства, чтобы сделать код более разборчивым


Теперь инструкция гласит: "Если каждая карточка становится уже 30ch, родительский контейнера перестраивается в одну колонку".


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




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


Почему бы просто не использовать Flexbox?


Дополнительным преимуществом использования CSS Grid вместо Flexbox для такого типа раскладки являются возможности выравнивания, которые мы получаем от Subgrid. Таким образом, мы можем обеспечить выравнивание содержимого (например, заголовков, футера и т.д) отдельных карточек, когда карточки расположены рядом. Вот как это выглядит:




Пока что subgrid поддерживается лишь браузером Firefox 75+.


Другие примеры использования математических функций


Применение математических функций в разметке открывает почти безграничные возможности. Еще одним примером использования математических функций может быть отзывчивый размер шрифта с использованием clamp() и без использования медиа-запросов.


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



Теперь же с помощью математических функций мы можем полностью устранить потребность в них.


font-size: clamp(
  var(--min-font-size) + 1px,
  var(--fluid-size),
  var(--max-font-size) + 1px /* мы добавляем 1px, чтобы значение определялось в пикселях */
);


Dave Rupert сделал нечто подобное, не прибегая к сложным вычислениям, используя при определении размера шрифта единицы viewport. Используя clamp() можно ограничить диапазон значений, гарантируя, что размер шрифта не станет слишком большим или маленьким.


h1 {
  --minFontSize: 32px;
  --maxFontSize: 200px;
  --scaler: 10vw;
  font-size: clamp(var(--minFontSize), var(--scaler), var(--maxFontSize));
}

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


Поддержка браузерами


На момент написания статьи, все современные браузеры поддерживают min(), max() и clamp(). Что касается Subgrid, к сожалению, он еще не стандартизован и работает только в Firefox.


Заключение


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