CSS: Отзывчивые макеты, меньше CSS медиа запросов

Источник: «Responsive Layouts, Fewer Media Queries»
Мы не можем говорить о веб-разработке, не упоминая Отзывчивый Дизайн. В наши дни это просто данность, и так было уже много лет. Медиа запросы — часть отзывчивого дизайна, и они никуда не денутся. С момента появления медиа-запросов (буквально десятилетия назад) CSS эволюционировал до такой степени, что существует множество трюков, которые могут нам помочь существенно сократить количество медиа-запросов. В некоторых случая я покажу вам, как заменить несколько медиа-запросов только одним CSS объявлением. Эти подходы могут привести к уменьшению объёма кода, упрощению обслуживания и большей степени привязаны к имеющемуся контенту.

Для начала рассмотрим некоторые широко используемые методы создания отзывчивых макетов без медиа запросов. Здесь нет сюрпризов — эти методы относятся к CSS Flexbox и CSS Grid.

Использование flex и flex-wrap

Посмотреть пример на CodePen.

В вышеприведённом примере flex: 400px устанавливает базовую ширину для каждого элемента сетки, равную 400px. Каждый элемент переносится на новую строку, если в текущей недостаточно места для его размещения. Между тем, элементы в каждой строке растут/растягиваются, что бы заполнить всё оставшееся место в контейнере, которое остаётся, если в ряду не может поместиться другой контейнер в 400px, и они сжимаются обратно до 400px, если другой элемент шириной 400px может туда втиснутся.

Давайте также вспомним, что flex: 400px сокращённый эквивалент: flex: 1 1 400px (flex-grow: 1, flex-shrink: 1, flex-base: 400px).

Что мы имеем на данный момент:

Использование auto-fit и minmax

Посмотреть пример на CodePen

Как и в предыдущем методе, мы устанавливаем базовую ширину благодаря repeat(auto-fit, minmax(400px, 1fr)) и наши элементы переносятся, если для них недостаточно места. На этот раз мы обратились к CSS Grid. Это означает, что элементы в каждой строке также увеличиваются, что бы заполнить оставшееся пространство. Но в отличие от конфигурации CSS Flexbox, последняя строка сохраняет ту же ширину, что и остальные элементы.

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

Оба метода, которые мы рассмотрели, хороши, но мы так же увидели, что у них есть ряд недостатков. Но мы можем преодолеть их с помощью некоторых CSS трюков.

Контролируем количество элементов в строке

Давайте возьмём наш первый пример и изменим flex: 400px, на flex: max(400px, 100% / 3-20px).

See the Pen

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

Давайте разберём код:

Вы также можете спросить: Что это за 20px в формуле?

Это в два раза увеличенный зазор в шаблонной сетке, то есть 10px умноженные на два. Когда у нас есть три элемента в строке, между элементами есть два промежутка (по одному слева и справа от центрального элемента), поэтому для N элементов мы должны использовать max(400px, 100% / N - (N - 1) * gap). Да, нам нужно учитывать зазор при определении ширины, но не волнуйтесь, мы всё равно можем оптимизировать формулу, что бы удалить его!

Мы можем использовать max(400px, 100% / (N + 1) + 0.1%). Логика такова: мы сообщаем браузеру, что каждый элемент имеет ширину, равную 100% / (N + 1), так N + 1 элемент в строке, но мы добавляем крошечный процент 0.1% — таким образом, один из элементов переносится и мы получаем только N элементов в строке. Умно, правда? Больше не беспокойтесь о зазоре!

See the Pen

Теперь мы можем контролировать максимальное количество элементов в строке, что даёт нам частичный контроль над количеством элементов в строке.

То же самое можно применить к методу CSS Grid:

See the Pen

Обратите внимание, что в примере я использовал CSS переменные для управления различными значениями.

Мы уже ближе!

Элементы растягиваются, но не сжимаются

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

max(400px, 100%/(N + 1) + 0.1%)

...на

clamp(100%/(N + 1) + 0.1%, 400px, 100%)

Разберём это:

See the Pen

Мы подходим ещё ближе!

Контролируем, когда элемент переносится

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

clamp(100%/(N + 1) + 0.1%, 400px, 100%)

на

clamp(100%/(N + 1) + 0.1%, (400px - 100vw)*1000, 100%)

Я слышу, как ты кричишь об этой сумасшедшей математике, но потерпи немного. Это проще, чем ты думаешь. Вот что происходит:

Посмотреть пример на CodePen

Мы сделали на первый медиа запрос без реального медиа запроса! Мы обновляем количество элементов в строке с N до 1 благодаря нашей формуле в clamp(). Следует отметить, что в этом случае 400px работаю как точка прерывания.

А как насчёт: от N до M элементов в строке

Мы можем это сделать обновив максимальную ширину контейнера в clamp():

clamp(100%/(N + 1) + 0.1%, (400px - 100vw)*1000, 100%/(M + 1) + 0.1%)

Я думаю, вы уже уловили суть этого CSS трюка. Когда ширина экрана больше 400px, мы попадаем под первое правило: 100%/(N + 1) + 0.1%. Когда ширина экрана больше 400px, мы попадаем под второе правило: 100%/(M + 1) + 0.1%

Посмотреть пример на CodePen

У нас получилось! Теперь мы можем контролировать количество элементов в строке и когда их количество должно изменится, используя только переменные и одно объявление CSS.

Больше примеров

Контролировать количество элементов между двумя значениями — это хорошо, но делать это для нескольких значений ещё лучше! Давайте попробуем перейти от N элементов в строке до M элементов в строке вплоть до одного элемента на строку.

Наша формула становится:

clamp(clamp(100%/(N + 1) + 0.1%, (W1 - 100vw)*1000,100%/(M + 1) + 0.1%), (W2 - 100vw)*1000, 100%)

clamp() внутри clamp()? Да, это становится очень длинным и сбивает с толку, но всё же лёгким для понимания. Обратите внимание на переменные W1 и W1. Поскольку мы меняем количество элементов в строках между тремя значениями, нам нужны две "точки прерывания" (с N на M и с M на 1).

Возможно так будет проще понять суть формулы с вложенным clamp():

clamp(
clamp( // Вложенный clamp(
100%/(N + 1) + 0.1%, // N элементов в строке,
(W1 - 100vw)*1000, // Сравниваем ширину экрана с W1,
100%/(M + 1) + 0.1%), // М элементов в строке)
(W2 - 100vw)*1000, // Сравниваем ширину экрана с W2,
100% // 1 элемент в строке,
)

Вот что происходит:

See the Pen

Мы сделали два медиа запроса, используя только одна CSS объявление! Не только это, но мы можем настроить это объявление благодаря переменным CSS, что означает, что у нас могут быть разные "точки прерывания" и разное количество столбцов для разных контейнеров.

See the Pen

Сколько медиа запросов у нас в приведённом выше примере? Слишком много, что бы сосчитать, но мы не будем останавливаться на достигнутом. Мы можем получить ещё больше, вложив ещё один clamp(), чтобы перейти от N столбцов к M столбцам к P и к одному столбцу.

clamp(
clamp(
clamp(100%/(var(--n) + 1) + 0.1%, (var(--w1) - 100vw)*1000,
100%/(var(--m) + 1) + 0.1%),(var(--w2) - 100vw)*1000,
100%/(var(--p) + 1) + 0.1%),(var(--w3) - 100vw)*1000,
100%), 1fr))
from N columns to M columns to P columns to 1 column

See the Pen

Как я упоминал в начале статьи, у нас есть отзывчивый макет без каких либо медиа запросов, использующий всего одно CSS объявление. Конечно, это длинное объявление, но всё равно считается за одно.

Посмотрим, что у нас есть:

Давайте симулируем контейнерные запросы

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

На момент написания этой статьи контейнерные запросы официально нигде не поддерживаются, но мы, безусловно, можем имитировать их с помощью нашей стратегии. Если мы изменим 100vw на 100% во всём коде, всё будет основано на ширине элемента .container, а не на ширине области просмотра. Это так просто!

Измените размер контейнеров и посмотрите на волшебство в игре.

See the Pen

Количество столбцов меняется в зависимости от ширины контейнера, что означает — мы имитируем контейнерные запросы! Мы действительно сделали это, просто заменив единицы области просмотра (vw) на относительное процентное значение (%).

Больше трюков

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

Условный цвет фона

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

Но я нашёл трюк, как сделать это без медиа запроса:

div {
background:
linear-gradient(green 0 0) 0 / max(0px,100px - 100%) 1px,
red;
}

Другими словами, мы сделали условие основанное на ширине элемента по сравнению со 100px!

See the Pen

Та же самая логика может основываться на высоте элемента, поставив вместо 1px: 1px max(0px,100px - 100%). Мы так же можем учитывать размер экрана используя vh или vw вместо %. Мы даже можем получить более двух цветов, добавив больше слоёв градиента.

div {
background:
linear-gradient(purple 0 0) 0 /max(0px,100px - 100%) 1px,
linear-gradient(blue 0 0) 0 /max(0px,300px - 100%) 1px,
linear-gradient(green 0 0) 0 /max(0px,500px - 100%) 1px,
red;
}
from N columns to M columns to P columns to 1 column

See the Pen

Переключение видимости элемента

Чтобы показать/скрыть элемент в зависимости от размера экрана, обычно мы используем медиа запрос и вставляем в него классический display: none. Вот ещё одна идея, которая имитирует то же поведение, только без медиа запроса.

div {
max-width: clamp(0px, (100vw - 500px) * 1000, 100%);
max-height: clamp(0px, (100vw - 500px) * 1000, 1000px);
overflow: hidden;
}

В зависимости от ширины экрана (100vw), мы либо получаем от clamp() значение в 0px для max-height и vax-width (что означает, что элемент скрыт), либо 100% (что означает, что элемент виден и никогда не превышает полной ширины). Мы не используем процентное соотношение для max-height, так как это не работает. Поэтому мы используем большое значение в пикселях (1000px)

Обратите внимание, как зелёные элементы исчезают на маленьких экранах.

See the Pen

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

Важно заметить, что я использую 0px, а не 0 внутри clamp() и max(). Последнее делает не валидным свойство. Я не буду вдаваться в подробности, но я ответил на вопрос на StackOverflow связанный с этой причудой, если вам нужны детали.

Изменения позиции элемента

Следующий CSS трюк полезен, когда мы имеем дело с фиксированным или абсолютно позиционированным элементом. Разница в том, что нам нужно обновить позицию в зависимости от ширины экрана. Как и в предыдущем трюке, мы по-прежнему полагаемся на clamp() и формулу, которая выглядит примерно так: clamp(X1, (100wv - W) * 1000, X2).

По сути, мы собираемся переключаться между значениями X1 и X2 в зависимости от разницы 100vw - W, где W — ширина, которая имитирует "точку прерывания".

Разберём пример. Мы хотим, чтобы блок <div> был размещён на левом краю (top: 50%; left: 0) когда размер экрана меньше 400px и перемещать его в другое место (например, top: 10%; left: 40%) в противном случае.

div {
--c:(100vw - 400px); /* we define our condition */
top: clamp(10%, var(--c) * -1000, 50%);
left: clamp(0px, var(--c) * 1000, 40%);
}

Посмотреть пример на CodePen

Во-первых, я определил условия с помощью переменных CSS, что бы избежать повторения. Обратите внимание, что я также использовал его с трюком переключения цвета фона, который мы рассматривали ранее — мы можем использовать либо 100vw - 400px, либо 400px - 100wv, но будьте внимательны в вычислениях, так как оба выражения имеют разные знаки.

Затем, в каждом clamp() мы всегда начинаем с наименьшего значения для каждого свойства. Не делайте неверного предположения, что сначала нужно указать значение для маленького экрана!

В конце, мы определяем знак для каждого условия. Я выбрал (100vw - 400px), что означает, что это значение будет отрицательным, если ширина экрана меньше 400px и положительным, когда ширина экрана больше 400px. Если мне нужно, что бы наименьшее значение clamp() считалось меньше 400px, я ничего не делаю со знаком выражения (оставляю его положительным), но если я хочу, что бы минимально значение считалось больше 400px — мне нужно инвертировать знак выражения. Вот почему вы видите (100vw - 400px) * - 1000 в свойстве top.

OK, я понимаю. Это не самая простая концепция, но давайте подойдём с другой стороны и проследим наши шаги, что бы лучше понять, что мы делаем.

Для свойства top у нас clamp(10%, (100vw - 400px) * - 1000, 50%) и так…

Такая же логика применима к тому, что мы объявляем для свойства left. Единственная разница в том, что мы умножаем на 1000 вместо -1000.

Небольшой секрет: на самом деле вам не нужна вся эта математика. Вы можете экспериментировать пока не получите идеальные значения, но для статьи мне нужно объяснить вещи таким образом, чтобы они приводили к последовательному поведению.

Следует отметить, что данный CSS трюк работает с любым свойством, которое принимает значение длинны (padding, margin, border-width, translate и т.д.). Мы не ограничены только изменениями позиции, мы можем изменять и другие свойства.

Примеры

Большинство из вас, вероятно задаются вопросом, можно ли вообще использовать какие-либо из этих концепций в реальных условиях. Позвольте мне показать вам несколько примеров, которые (надеюсь) убедят вас в этом.

Шкала прогресса

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

На момент написания, этот пример поддерживался Chrome, Edge и Firefox.

See the Pen

Это довольно простой пример, в котором я задаю три диапазона:

Здесь нет "дикого CSS" или JavaScript для обновления цвета. "Волшебное" свойство background позволяет нам задать динамический цвет, который изменяется в зависимости от вычисленных значений.

Редактируемый контент

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

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

На момент написания, этот пример поддерживался Chrome, Edge и Firefox.

See the Pen

Макет временной шкалы

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

See the Pen

Когда ширина экрана меньше 600px, все псевдо элементы удаляются, изменяя макет с двух столбцов на один столбец. Затем цвет меняется с шаблона синий/зелёный/зелёный/синий на синий/зелёный шаблон.

Отзывчивая карточка

Вот подход с отзывчивой карточкой, при котором свойства CSS обновляются в зависимости от размера области просмотра. Обычно мы можем ожидать переход макета от двух столбцов к одному на маленьких экранах, где изображение карточки размещается либо над, либо под содержимым. В этом примере, мы меняем такие свойства, как position, width, height, padding и border-radius, чтобы получить совершенно другой макет, в котором изображение размещается рядом с заголовком карточки.

See the Pen

Пузыри сообщений / выноски

Нужны красивые отзывы о вашем продукте или услуге? Эти отзывчивые выноски работают практически где угодно, даже без медиа запросов.

See the Pen

Зафиксированная кнопка

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

See the Pen

Зафиксированное уведомление

Ещё один пример, на этот раз что-то что может работать с уведомлениями "Общего регламента по защите данных", предупреждающих о куки-файлах:

See the Pen

Заключение

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

Мы рассмотрели CSS Flexbox и CSS Grid, clamp(), относительные единицы и объединили их вместе, чтобы делать самые разные вещи, от изменения фона элемента в зависимости от ширины его контейнера, изменения позиции при определённых размерах экрана и даже имитацию ещё не вышедших контейнерных запросов. Интересный материал! И всё это без единого @media в CSS.

Цель не в том, что бы избавится или заменить CSS медиа запросы, а в том, что бы оптимизировать и уменьшить объём кода, потому что CSS сильно изменился и у нас есть мощный инструмент для создания условных стилей. Другими слова, замечательно видеть, как набор возможностей CSS расширяется таким образом, чтобы облегчить нашу жизнь как фронтэндера одновременно наделяя нас сверх способностями контроля поведения наших макетов, как никогда ранее.

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

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

Прилипающие CSS Grid элементы

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

CSS: Пропорциона­ль­ное масштабиро­ва­ние с css-переменными