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)
.
Измените размер экрана и обратите внимание, что в ни в одной строке никогда не бывает более трёх элементов, даже на сверх широком экране. Мы ограничили каждую строку максимум тремя элементами, то есть каждая строка может содержать от одного до трёх элементов в любой момент времени.
Давайте разберём код:
- Когда ширина экрана увеличивается, ширина нашего контейнера также увеличивается, это означает, что
100% / 3
в какой-то момент становится больше 400px. - Поскольку мы используем функцию
max()
в качестве ширины и делим в ней100%
на3
, максимальный размер любого отдельного элемента составляет лишь одну треть от общей ширины контейнера. Итак, мы получаем максимум три элемента в строке. - Когда ширина экрана маленькая, вперёд идёт
400px
, и мы получаем наше первоначальное поведение.
Вы также можете спросить: Что это за 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
элементов в строке. Умно, правда? Больше не беспокойтесь о зазоре!
Теперь мы можем контролировать максимальное количество элементов в строке, что даёт нам частичный контроль над количеством элементов в строке.
То же самое можно применить к методу CSS Grid:
Обратите внимание, что в примере я использовал CSS переменные для управления различными значениями.
Мы уже ближе!
- ✔️ Всего одна строка кода
- ✔️ Согласованная ширина элемента с футером
- ⚠️ Частичный контроль элементов в строке
- ❌ Элементы растягиваются, но не сжимаются
- ❌ Контролируем, когда элемент переносится
Элементы растягиваются, но не сжимаются
Ранее мы отмечали, что использование CSS Grid метода может привести к переполнению, если базовая ширина больше, чем ширина контейнера. Чтобы исправить это, мы изменим нашу формулу с:
max(400px, 100%/(N + 1) + 0.1%)
...на
clamp(100%/(N + 1) + 0.1%, 400px, 100%)
Разберём это:
- Когда ширина экрана большая, функция
clamp()
возвращает значение400px
для формулы100%/(N + 1) + 0.1%
, что позволяет нам контролировать максимальное количество элементов в строке. - Когда ширина экрана маленькая, функция
clamp()
возвращает значение100%
, поэтому наши элементы никогда не превышают ширину контейнера.
Мы подходим ещё ближе!
- ✔️ Всего одна строка кода
- ✔️ Согласованная ширина элемента с футером
- ⚠️ Частичный контроль элементов в строке
- ✔️ Элементы растягиваются и сжимаются
- ❌ Контролируем, когда элемент переносится
Контролируем, когда элемент переносится
До сих пор, мы не контролировали, когда элементы переносятся с одной строки на другую. Мы действительно не знаем, когда это произойдёт, потому что это зависит от ряда вещей, таких как базовая ширина, отступы, ширина контейнера и т.д. Что бы контролировать это, мы изменим нашу формулу в clamp()
, с этого:
clamp(100%/(N + 1) + 0.1%, 400px, 100%)
на
clamp(100%/(N + 1) + 0.1%, (400px - 100vw)*1000, 100%)
Я слышу, как ты кричишь об этой сумасшедшей математике, но потерпи немного. Это проще, чем ты думаешь. Вот что происходит:
- Когда ширина экрана (
100vw
) больше400px
,(400px - 100vw)
даёт отрицательное значение иclamp()
возвращает100% / (N + 1) + 0.1%
, что является положительным значением. Это даёт нам N элементов в строке. - Когда ширина экрана (
100vw)
меньше400px
,(400px - 100vw)
даёт положительное значение и умножается на большое значение, вследствие чегоclamp()
возвращает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 элемент в строке,
)
Вот что происходит:
- Когда ширина экрана меньше, чем W2,
clamp()
возвращает100%
или один элемент на строку. - Когда ширина экрана больше
W2
, мы используем результат из вложенногоclamp()
. - Во вложенном
clam()
, когда ширина экрана меньше, чемW1
, получаем ширину100%/(M + 1) + 0.1%
илиM
элементов в строке. - Во вложенном
clamp()
, когда ширина экрана больше, чемW1
, получаем ширину100%/(N + 1) + 0.1%
илиN
элементов в строке.
Мы сделали два медиа запроса, используя только одна CSS объявление! Не только это, но мы можем настроить это объявление благодаря переменным CSS, что означает, что у нас могут быть разные "точки прерывания" и разное количество столбцов для разных контейнеров.
Сколько медиа запросов у нас в приведённом выше примере? Слишком много, что бы сосчитать, но мы не будем останавливаться на достигнутом. Мы можем получить ещё больше, вложив ещё один 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))
Как я упоминал в начале статьи, у нас есть отзывчивый макет без каких либо медиа запросов, использующий всего одно CSS объявление. Конечно, это длинное объявление, но всё равно считается за одно.
Посмотрим, что у нас есть:
- ✔️ Всего одна строка кода
- ✔️ Согласованная ширина элемента с футером
- ✔️ Полный контроль над количеством элементов в строке
- ✔️ Элементы растягиваются и сжимаются
- ✔️ Контролируется когда элемент переносится
- ✔️ Легко обновляется с помощью CSS переменных
Давайте симулируем контейнерные запросы
Все в восторге от контейнерных запросов! Что делает их аккуратными, так это то, что они учитывают ширину элемента, а не область просмотра или экрана. Идея состоит в том, что элемент может адаптироваться в зависимости от ширины его родительского контейнера для более точного контроля над тем, как элементы реагируют на различные контексты.
На момент написания этой статьи контейнерные запросы официально нигде не поддерживаются, но мы, безусловно, можем имитировать их с помощью нашей стратегии. Если мы изменим 100vw
на 100%
во всём коде, всё будет основано на ширине элемента .container
, а не на ширине области просмотра. Это так просто!
Измените размер контейнеров и посмотрите на волшебство в игре.
Количество столбцов меняется в зависимости от ширины контейнера, что означает — мы имитируем контейнерные запросы! Мы действительно сделали это, просто заменив единицы области просмотра (vw
) на относительное процентное значение (%
).
Больше трюков
Теперь, когда мы можем контролировать количество столбцов, давайте рассмотрим другие примеры, которые позволяют нам создавать условия CSS на основе размера экрана (или элемента).
Условный цвет фона
Не так давно, кто-то на StackOverflow спрашивал, можно ли менять цвет элемента в зависимости от его ширины или высоты. Многие говорили, что невозможно или что для этого потребуется медиа запрос.
Но я нашёл трюк, как сделать это без медиа запроса:
div {
background:
linear-gradient(green 0 0) 0 / max(0px,100px - 100%) 1px,
red;
}
- У нас есть слой с линейным градиентом и шириной равной
max(0px, 100px - 100%)
и высотой1px
. Высота не имеет значения, поскольку градиент повторяется по умолчанию. Кроме того, это однотонный градиент, поэтому подойдёт любая высота. - 100% относится к ширине элемента. Если в вычислении
100%
больше100px
,max()
вернёт0px
, что означает, что градиент не отображается, но красный фон, отделённый запятой отображается. - Если вычисленные
100%
оказываются меньше100px
, градиент отображается, и вместо этого мы получаем зелёный фон.
Другими словами, мы сделали условие основанное на ширине элемента по сравнению со 100px
!
Та же самая логика может основываться на высоте элемента, поставив вместо 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;
}
Переключение видимости элемента
Чтобы показать/скрыть элемент в зависимости от размера экрана, обычно мы используем медиа запрос и вставляем в него классический 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
)
Обратите внимание, как зелёные элементы исчезают на маленьких экранах.
Следует отметить, что этот метод не эквивалентен переключению отображаемого значения. Это скорее трюк, задать элементу размеры 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%)
и так…
- если ширина экрана (
100vw
) меньше400px
, то результат разницы100vw - 400px
даст отрицательную величину. Мы умножаем его на другое большое (относительно знака) отрицательное значение (в данном случае-1000
), что бы получить большое положительное значение, в результате чегоclamp()
вернёт значение50%
: Это означает, что мы получаемtop: 50%
когда ширина экрана меньше400px
. - если ширина экрана (
100vw
), больше400px
, мы получаемtop: 10%
.
Такая же логика применима к тому, что мы объявляем для свойства left
. Единственная разница в том, что мы умножаем на 1000
вместо -1000
.
Небольшой секрет: на самом деле вам не нужна вся эта математика. Вы можете экспериментировать пока не получите идеальные значения, но для статьи мне нужно объяснить вещи таким образом, чтобы они приводили к последовательному поведению.
Следует отметить, что данный CSS трюк работает с любым свойством, которое принимает значение длинны (padding
, margin
, border-width
, translate
и т.д.). Мы не ограничены только изменениями позиции, мы можем изменять и другие свойства.
Примеры
Большинство из вас, вероятно задаются вопросом, можно ли вообще использовать какие-либо из этих концепций в реальных условиях. Позвольте мне показать вам несколько примеров, которые (надеюсь) убедят вас в этом.
Шкала прогресса
Трюк с изменением цвета фона позволяет получить отличный индикатор прогресса или любой другой аналогичный элемент, где нам нужно отображать другой цвет в зависимости от прогресса.
На момент написания, этот пример поддерживался Chrome, Edge и Firefox.
Это довольно простой пример, в котором я задаю три диапазона:
- Красный: [0% 30%]
- Оранжевый: [30% 60%]
- Зелёный: [60% 100%]
Здесь нет "дикого CSS" или JavaScript для обновления цвета. "Волшебное" свойство background
позволяет нам задать динамический цвет, который изменяется в зависимости от вычисленных значений.
Редактируемый контент
Часто пользователям дают возможность редактировать контент. Мы можем изменять цвета в зависимости от того, что введено.
В следующем примере мы получаем жёлтое "предупреждение" при вводе более трёх строк текста и красное "предупреждение", если мы ввели более шести строк. Это позволит уменьшить количество JavaScript кода определяющего высоту элемента, а затем добавляющего/удаляющего определённый класс.
На момент написания, этот пример поддерживался Chrome, Edge и Firefox.
Макет временной шкалы
Временные шкалы — отличные шаблоны для визуализации ключевых моментов во времени. Эта реализация использует три CSS трюка, что бы получить один без каких либо CSS медиа запросов. Один трюк — это обновление количества столбцов, второй — скрытие некоторых элементов на маленьких экранах и последний — обновление цвета фона. Опять же, без CSS медиа запросов!
Когда ширина экрана меньше 600px
, все псевдо элементы удаляются, изменяя макет с двух столбцов на один столбец. Затем цвет меняется с шаблона синий/зелёный/зелёный/синий
на синий/зелёный
шаблон.
Отзывчивая карточка
Вот подход с отзывчивой карточкой, при котором свойства CSS обновляются в зависимости от размера области просмотра. Обычно мы можем ожидать переход макета от двух столбцов к одному на маленьких экранах, где изображение карточки размещается либо над, либо под содержимым. В этом примере, мы меняем такие свойства, как position
, width
, height
, padding
и border-radius
, чтобы получить совершенно другой макет, в котором изображение размещается рядом с заголовком карточки.
Пузыри сообщений / выноски
Нужны красивые отзывы о вашем продукте или услуге? Эти отзывчивые выноски работают практически где угодно, даже без медиа запросов.
Зафиксированная кнопка
Вы знаете эти кнопки, которые иногда прикрепляются к левому или правому краю экрана и обычно используются для обратной связи или опроса? Мы можем разместить одну из них на большом экране, затем трансформировать её в круглую кнопку, закреплённую в нижнем правом углу на маленьких экранах, для более удобного нажатия.
Зафиксированное уведомление
Ещё один пример, на этот раз что-то что может работать с уведомлениями "Общего регламента по защите данных", предупреждающих о куки-файлах:
Заключение
Медиа запросы были основным ингредиентом отзывчивого дизайна с тех пор как, термин отзывчивый дизайн был придуман много лет назад. Хотя они определённо никуда не денутся, мы рассмотрели несколько новый CSS функций и концепций, которые позволяют реже полагаться на медиа запросы для создания отзывчивых макетов.
Мы рассмотрели CSS Flexbox и CSS Grid, clamp()
, относительные единицы и объединили их вместе, чтобы делать самые разные вещи, от изменения фона элемента в зависимости от ширины его контейнера, изменения позиции при определённых размерах экрана и даже имитацию ещё не вышедших контейнерных запросов. Интересный материал! И всё это без единого @media
в CSS.
Цель не в том, что бы избавится или заменить CSS медиа запросы, а в том, что бы оптимизировать и уменьшить объём кода, потому что CSS сильно изменился и у нас есть мощный инструмент для создания условных стилей. Другими слова, замечательно видеть, как набор возможностей CSS расширяется таким образом, чтобы облегчить нашу жизнь как фронтэндера одновременно наделяя нас сверх способностями контроля поведения наших макетов, как никогда ранее.