Маленькие хитрости кастомных свойств (CSS-переменных)
Поводом для этой заметки стал недавний твит нашего давнего знакомого Зака Лезермана, лучшего в мире знатока веб-шрифтов:
Сегодня у меня возникла потребность в условных операциях с кастомными свойствами CSS.
flex-basis: (—my-variable ? 0 : 4px)
Я не смог сделать это с фолбэками в var(). Это возможно или обсуждается где-нибудь среди стандартистов? (CSS-in-JS — ответ не на тот вопрос)
Другими словами, нужно выбирать ту или иную величину в зависимости от наличия переменной, независимо от ее значения.
И я решил поделиться несколькими находками, связанными с неочевидными (по крайней мере для меня:) особенностями CSS-переменных. Можно считать это еще одним уроком CSSbattle – именно там я на них впервые наткнулся. Возможно, и вам они где-нибудь пригодятся:)
1. Разные дефолтные значения для одной переменной
Без лишних предисловий, вот мое решение задачи Зака:
flex-basis: calc(var(--my-variable, 4px) - var(--my-variable, 0px));
Принцип простой: когда переменная --my-variable
задана, внутри calc()
она «вычитается сама из себя», и получается ноль. Если же она не задана, то из дефолтного («фолбэчного») значения первого var()
вычитается дефолтное значение второго var()
, а они разные. И их разность — как раз требуемые 4px. Ведь фолбэк задается не для переменной, а для функции var()
, т.е. для каждого обращения к переменной!
Увидеть это в действии можно в простейшем примере на JSfiddle. И в более продвинутом примере самого Зака: https://codepen.io/zachleat/pen/KKKmqEG.
Чем не дополнение к арсеналу логических приемов с CSS-переменными наподобие таких трюков Аны Тюдор?
Как всегда, есть пара нюансов. Во-первых, 0px
во втором var()
нельзя сократить до 0
: внутри calc()
это разные вещи — первое длина, второе безразмерное число, и вычитать их друг из друга нельзя (как нельзя вычитать килограммы из сантиметров:). Во-вторых, само значение переменной --my-variable
тоже должно быть с размерностью длины (либо процентами). Причина та же — особенности работы calc()
: размерность в ней имеет значение всегда, и 0px
— далеко не то же самое, что, скажем, 0deg
. Первое подходит по типу для свойства flex-basis, второе нет. Правила этой проверки типов описаны в спецификации CSS Values and Units. На недавней FrontendConf 2019 был замечательный доклад Софии Валитовой на эту тему.
В CSSbattle прием с разными дефолтными значениями для разных var()
не раз помогал мне одним «выстрелом» (определением переменной) переопределить сразу несколько значений для вложенного контейнера. Например, следующий пример (после варварской минификации) принес мне третье место в одном из заданий:
* { /* применяется и к html, и к body */ margin: 50px var(--, 83% 99px -8%); box-shadow: 7em var(--, 0 #aa445f), 63vw var(--, 52q #e38f66); /* еще какие-то общие стили… */ } * + * { /* применяется к body */ --: 0 0; }
2. Разный контекст для одной переменной
Если имя переменной в предыдущем примере и то, что она применяется к двум разным свойствам, показалось вам странным — не удивляйтесь. По опубликованной версии спецификации --
(да, лишь два дефиса) было допустимым именем кастомного свойства (правда, потом Рабочая группа по CSS решила его зарезервировать, и в Firefox оно уже не работает!). И объявления с кастомными свойствами считаются «валидными» до самого этапа вычисленного значения (ведь нельзя знать заранее, что занесет туда стихией каскада!). В сочетании с сокращенными свойствами это дает массу неожиданных возможностей.
В спецификации, как обычно, про это написано очень сложно, но в сухом остатке получается, что значение просто подставляется на место переменной и получившаяся строка по сути парсится каждый раз заново. В примере выше у body получается margin: 50px 0 0; box-shadow: 7em 0 0, 63vw 0 0;
— оба значения корректны (цветом теней будет currentColor
, последний ноль, размытие, выглядит лишним, но и вреда от него нет).
Особенно хорошо это видно на сокращениях, вмещающих в себя массу разных подсвойств, типа background
. Вот другой мой пример с CSSbattle, достигший призового места, «разминифицированный» для наглядности:
* { /* применяется и к html, и к body */ background: var(--hack, #6592cf 0 -57%/50%) 504q /* цвет фона, положение картинки/размер картинки */ radial-gradient(1q at 50% var(--hack, 100%), /* градиент в форме круга с центром по середине нижнего края */ #0000 42q, #060f55 0 64q, #0000); /* в виде синего кольца с прозрачностью внутри и снаружи */ } * + * { /* применяется к body */ --hack: 0; }
Там, где для html
была задана целая куча подсвойств фона — и фоновый цвет, и позиция по обеим координатам, и размер по горизонтали — для body
остался только 0. И это меняет не только значения цвета фона и размера картинки-градиента (на дефолтные прозрачный и 100% соответственно), но и интерпретацию 504q
, стоящих снаружи var()
: теперь перед ним нет слеша, а значит, это уже не размер (по вертикали), а позиция! А в другом var()
это же самое переопределение передвинуло центр градиента с нижнего края к верхнему, «перевернув» полукольцо.
Конечно, такая «магия», уместная в развлекательном состязании, изрядно попахивает хаком, и в реальных проектах ей вряд ли найдется место. Но «превратить» так одно подсвойство в другое можно и случайно, так что полезно знать, что так бывает, чтобы избежать сюрпризов (пожалуй, это еще один аргумент против сокращенных свойств:).
Пустое значение переменной (добавлено 5.11.2019)
Ана Тюдор показала в Твиттере еще один пример полезного использования этой особенности CSS-переменных, и я решил упомянуть очередной важный нюанс в связи с этим. Значение CSS-переменной, используемой как часть составного значения свойства, можно сделать пустым. Например, в примере Аны: grid-template-columns: var(--c0) var(--c1) var(--c2);
– можно для узкого экрана превратить трехколоночный грид в двухколоночный, «уничтожив» среднюю колонку: --c1: ;
.
А нюанс здесь в том, что по последней опубликованной версии спецификации пробел после двоеточия имеет значение, и надо следить, чтобы минификация его «не съела». Если --c1: ;
– это корректное присваивание переменной пустого значения (пустой строки), то --c1:;
(без пробела) – некорректное объявление, которое браузер просто отбросит. Будьте внимательны!
В текущем редакторском черновике этот нелогичный нюанс убрали, но в браузерах (как минимум, Chrome 78-) он еще актуален.
3. Переопределение значений в обход специфичности селекторов
Вот часть моего решения на CSSbattle (без минификации), которому удалось добраться до рекорда:
*, p { padding:var(--padding, 17px 67px); } * > * { /* применяется к body и всем его потомкам */ --padding: 50px; } * > * > * { /* применяется только к потомкам body */ --padding: 37.5px; }
Структура разметки (точнее, DOM) такова: html
> body
> p
> img
.
Может показаться, что у абзаца должны быть отступы как у корневого элемента, поскольку специфичность селектора по тегу (p
) выше, чем у любой комбинации из универсальных селекторов. Но ведь фолбэк применяется только тогда, когда в каскаде для элемента нет подходящего значения переменной. А у нас оно есть — 37.5px
! Благодаря этому удалось «сэкономить» селектор, а с ним и общий размер решения.
Не может ли такой «фокус» пригодиться нам и в обычной разработке? Например, вместо
.block__button { /* какие-то другие стили кнопки */ color: black; } .block__button--danger { color: red; }
использовать
.block__button { /* прочие стили кнопки */ color: var(--button-color, black); } .block__button--danger { --button-color: red; }
и тем самым устранить зависимость от последовательности этих классов в коде? Что думаете? :)
P.S. Это тоже может быть интересно:
Последний примерихорош, но надо хорошо называть тогда переменные, чтобы не ошибиться.