Почему я в восторге от «родных» CSS-переменных

Перевод статьи Why I’m Excited About Native CSS Variables  с сайта philipwalton.com, опубликовано на css-live.ru с разрешения автора — Филипа Уолтона.

Несколько недель назад CSS-переменные — точнее, пользовательские CSS-свойства — стали доступны в Chrome Canary за флагом «Экспериментальные возможности веб-платформы».[1]

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

После беглого просмотра откликов стало ясно, что 99% процентов жалоб сосредоточены вокруг двух моментов:

  • «Страшный» и «многословный» синтаксис
  • Уже есть переменные в Sass, зачем что-то еще?

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

А что касается сравнения CSS-переменных с переменными Sass, то вот в чем, по-моему, кроется главное непонимание:

«Родные» CSS-переменные и не пытались лишь копировать то, что уже по силам CSS-препроцессорам. На самом деле, если почитать некоторые обсуждения исходной идеи, вы увидите, что в первую очередь «родные» CSS-переменные задумывались ради возможности делать то, что недоступно препроцессорам!

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

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

Примечание: эта статья —  не введение в пользовательские CSS-свойства. Если вы не слыхали про них или не в курсе, как они работают, советую сначала ознакомиться с ними.

Ограниченность препроцессорных переменных

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

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

Переменные препроцессоров не динамичны

Пожалуй, самый частый пример ограниченности препроцессоров, который ставит в тупик начинающих — то, что Sass не умеет определять переменные или использовать @extend внутри медиавыражения. Раз статья о переменных, я сосредоточусь на первом:

$gutter: 1em;

@media (min-width: 30em) {
  $gutter: 2em;
}

.Container {
  padding: $gutter;
}

Если скомпилировать этот код, получится вот что:

.Container {
  padding: 1em;
}

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

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

Поскольку менять переменную на основе соответствующего правила @media нельзя, единственный выход — заводить уникальную переменную на каждое медиавыражение, и писать код для каждого варианта отдельно. Подробнее об этом позже.

Переменные препроцессоров не каскадируются

Где бы ни использовались переменные, рано или поздно встает вопрос области видимости. Должна ли эта переменная быть глобальной? Должна ли она ограничиваться своим файлом/модулем? Или блоком?

Поскольку CSS в конце концов оформляет HTML, оказывается, что есть еще один полезный способ ограничивать видимость переменных: на уровне DOM-элемента. Но раз препроцессоры работают не в браузере и никогда не видят разметки, то им это недоступно.

Возьмем сайт, который пытается добавлять класс user-setting-large-text к элементу <html> для пользователей, выбравших увеличенный текст. Когда этот класс задан, переменной $font-size должно присвоиться большее значение:

$font-size: 1em;

.user-setting-large-text {
  $font-size: 1.5em;
}

body {
  font-size: $font-size;
}

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

body {
  font-size: 1em;
}

Переменные препроцессоров не наследуются

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

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

.alert { background-color: lightyellow; }
.alert.info { background-color: lightblue; }
.alert.error { background-color: orangered; }

.alert button {
  border-color: darken(background-color, 25%);
}

Это не валидный код Sass (тем более CSS), но вы, наверное, поняли, чего этим кодом пытались добиться.

Последнее объявление пытается применить Sass-овую функцию darken к свойству background-color, которое элемент <button> мог унаследовать от своего родительского элемента .alert. Если к выпадающему сообщению добавили класс info или error (либо цвет фона произвольно изменили скриптом или пользовательскими стилями), кнопке надо уметь подстраиваться под это.

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

Вот один важный случай, который стоит выделить: было бы крайне удобно, если можно было бы применять цветовые функции к унаследованным DOM-свойствам — ради доступности. Например, чтобы сделать текст всегда заведомо читаемым и достаточно контрастным относительно фона. С пользовательскими свойствами и новыми функциями цвета в CSS это вот-вот станет возможным!

Препроцессорные переменные не универсальны

Это достаточно очевидный недостаток препроцессоров, но я упомяну его, так как по-моему он важен. Если вы делаете сайт с PostCSS и вам понадобился сторонний компонент, оформление которого настраивается только через Sass — вам не повезло.

Нельзя (как минимум, непросто) использовать общие переменные препроцессоров для разных инструментов или для сторонних стилей, размещенных на CDN.

«Родные» CSS-переменные будут работать с любым CSS-препроцессором или даже статичным CSS-файлом. В отличие от препроцессорных.

В чем отличие пользовательских свойств

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

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

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

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

Примеры из жизни

Если вам еще не очень понятно, что это за задачи, с которыми пользовательские свойства справляются, а переменные препроцессоров — нет, то вот для вас несколько примеров.

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

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

Отзывчивые свойства с медиавыражениями

Многие сайты используют переменную «gap» или «gutter», которая определяет зазор по умолчанию между блоками в раскладке, а также отступы по умолчанию для всех разделов страницы. Чаще всего хочется, чтобы этот зазор зависел от размера окна браузера. На больших экранах нужно больше пространства между элементами («больше воздуха»), но на маленьких экранах столько места взять негде, поэтому зазоры должны быть меньше.

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

Следующий пример определяет переменные $gutterSm, $gutterMd и $gutterLg, а затем объявляет разные правила для каждого варианта:

/* Объявляет три значения зазора, по одному для каждого размера экрана */

$gutterSm: 1em;
$gutterMd: 2em;
$gutterLg: 3em;

/* Базовые стили для маленьких экранов, с $gutterSm. */

.Container {
  margin: 0 auto;
  max-width: 60em;
  padding: $gutterSm;
}
.Grid {
  display: flex;
  margin: -$gutterSm 0 0 -$gutterSm;
}
.Grid-cell {
  flex: 1;
  padding: $gutterSm 0 0 $gutterSm;
}

/* Переопределят стили для средних экранов, с $gutterMd. */

@media (min-width: 30em) {
  .Container {
    padding: $gutterMd;
  }
  .Grid {
    margin: -$gutterMd 0 0 -$gutterMd;
  }
  .Grid-cell {
    padding: $gutterMd 0 0 $gutterMd;
  }
}

/* Переопределяем стили для больших экранов, с $gutterLg. */

@media (min-width: 48em) {
  .Container {
    padding: $gutterLg;
  }
  .Grid {
    margin: -$gutterLg 0 0 -$gutterLg;
  }
  .Grid-cell {
    padding: $gutterLg 0 0 $gutterLg;
  }
}

Чтобы добиться абсолютно того же самого с пользовательскими свойствами, достаточно объявить стили всего однажды. Можно воспользоваться единственным свойством --gutter, а затем при изменениях размеров окна обновлять его значение, и всё само  подстроится соответственно.

/* Объявляет, чему равно `--gutter` при каждом размере экрана */

:root { --gutter: 1.5em; }

@media (min-width: 30em) {
  :root { --gutter: 2em; }
}
@media (min-width: 48em) {
  :root { --gutter: 3em; }
}

/*
* Стили нужно определять только один раз, так как
* пользовательские свойства автоматически обновляются.
 */

.Container {
  margin: 0 auto;
  max-width: 60em;
  padding: var(--gutter);
}
.Grid {
  --gutterNegative: calc(-1 * var(--gutter));
  display: flex;
  margin-left: var(--gutterNegative);
  margin-top: var(--gutterNegative);
}
.Grid-cell {
  flex: 1;
  margin-left: var(--gutter);
  margin-top: var(--gutter);
}

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

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

custom-properties-responsive-1400wСмотреть пример на CodePen: в режиме редактора / на всю страницу

Оформление в зависимости от контекста

Оформление в зависимости от контекста (разные стили элемента в зависимости от того, где он окажется в DOM) в CSS — тема жарких споров. С одной стороны, авторитетные CSS-разработчики призывают такого избегать. Но с другой стороны, большинство до сих пор каждый день это делает.

Гарри Робертс недавно поделился своими соображениями по теме в этой статье:

Если вам нужно по-разному украшать интерфейсный компонент в зависимости от того, где он находится, то с вашим подходом к дизайну что-то не так… Надо проектировать вещи так, чтобы им не нужно было знать об окружении, так, чтобы у нас всегда был просто «этот компонент», а не «этот компонент внутри…»

Хоть я и поддерживаю Гарри в этом (и многом другом), я думаю, что такая популярность «кратчайшего» пути решения подобных задач указывает на большую проблему: языку CSS не хватает выразительности, и ни один из существующих «правильных подходов» не устраивает людей до конца.

Следующий пример показывает, как большинство подходит к оформлению по контексту в CSS, используя селектор потомка:

/* Стили обычной кнопки. */
.Button { }

/* Стили кнопки, находящейся в «шапке», отличаются. */
.Header .Button { }

У этого подхода много проблем (которые я объясняю в статье об архитектуре CSS). Одна из причин считать этот прием «запашком» кода — он нарушает принцип открытости/закрытости: при нем изменяются детали реализации закрытого компонента.

Программные сущности (классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для изменения.

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

.Button {
  background: var(--Button-backgroundColor, #eee);
  border: 1px solid var(--Button-borderColor, #333);
  color: var(--Button-color, #333);
  /* ... */
}

.Header {
  --Button-backgroundColor: purple;
  --Button-borderColor: transparent;
  --Button-color: white;
}

Отличие этого примера от предыдущего тонкое, но важное.

С селектором потомка мы объявляем, что кнопки в «шапке» будут выглядеть вот так, и это отличается от того, как определяется сам компонент кнопки. Это какое-то диктаторское объявление (по меткому выражению Гарри), и его трудно отменить, если в каком-то исключительном случае кнопка в «шапке» не должна так выглядеть.

С пользовательскими же свойствами компонент кнопки по-прежнему ничего не знает о контексте и никак не сцеплен с компонентом «шапки». Его объявление всего лишь говорит: «Я буду применять к себе стили на основе этих пользовательских свойств, какими бы они ни были здесь и сейчас». А компонент «шапки» говорит всего лишь: «Я собираюсь задать такие значения свойств; мои потомки сами решат, как их использовать и использовать ли вообще».

Главное отличие в том, что компонент кнопки сам решает, надо ли его расширять, и это легко отменить для какого-нибудь исключения.

Следующий пример показывает оформление по контексту для ссылок и кнопок как в «шапке» сайта, так и в контентной области.

custom-properties-contextual-styling-1400wСмотреть пример на CodePen: в режиме редактора / на всю страницу

Исключительные случаи

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

С селекторами потомков вам пришлось бы написать кучу стилей для кнопок в «шапке», а затем отменить эти стили для кнопок в промоблоке, что и выглядит неаккуратно, и ошибиться с этим легко, а с ростом вложенности оно вообще идет вразнос.

/* Стили обычной кнопки. */
.Button { }

/* Стили кнопки, находящейся в «шапке», отличаются. */
.Header .Button { }

/* Отмена стилей тех кнопок в «шапке», которые еще и в промоблоке. */
.Header .Promo .Button { }

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

.Promo {
  --Button-backgroundColor: initial;
  --Button-borderColor: initial;
  --Button-color: initial;
}

Полезный опыт из React

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

Но мое мнение стало меняться, когда я сравнил пользовательские свойства в CSS c props в React.

Эти props из React — тоже динамические переменные с видимостью, привязанной к DOM, и они наследуются, благодаря чему компоненты могут становиться контекстозависимыми. В React родительские компоненты передают данные дочерним, а дочерние уже определяют, какие props они готовы принять и что собираются с ними делать. Эта архитектурная модель широко известна как однонаправленный поток данных.

Даже с учетом того, что пользовательские свойства — новая, неизведанная область, по-моему, успех модели React убедительно доказывает, что можно строить сложные системы на базе наследования свойств. И более того, что переменные с видимостью на уровне DOM — полезный паттерн разработки.

Минимизация побочных эффектов

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

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

.MyComponent {
  --propertyName: initial;
}

Хоть это еще не вошло в спецификацию, одно время обсуждалось[2] свойство --, которое могло бы сбрасывать все пользовательские свойства. А чтобы выборочно разрешить наследование лишь некоторых свойств, можно отдельно указать для них inherit, благодаря чему они будут и дальше действовать как обычно:

.MyComponent {
 /* Сбрасывает все пользовательские свойства. */
  --: initial;

 /* Разрешает наследование только следующих польз. свойств */
  --someProperty: inherit;
  --someOtherProperty: inherit;
}

Управление глобальными именами

Если вы обратили внимание, как я называю свои пользовательские свойства, вы могли заметить, что мои свойства для конкретных компонентов начинаются с имени класса самого компонента, напр. --Button-backgroundColor.

Как и большинство имен в CSS, пользовательские свойства глобальны, и всегда есть риск конфликта имен с другими переменными, с которыми работают ваши коллеги.

Простой способ избежать этой проблемы — договориться о системе именования, например так, как я показал выше.

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

Итого

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

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

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

Я не думаю, что это должен быть выбор «или — или». И противопоставлять их друг другу как конкурентов никому не нужно.

Особые благодарности Эдди Османи и Мэтту Гонту за рецензирование этой статьи, а также Шейну Стивенсу за то, что обратил внимание на баг Chrome и исправил его, чтобы демо-примеры заработали.

Примечания:

  1. Чтобы включить флаг «Экспериментальные возможности веб-платформы» в Chrome, откройте страницу about:flags, найдите  «Экспериментальные возможности веб-платформы» и нажмите кнопку «включить».
  2. На полезность свойства -- (в связи с оформлением пользовательских элементов) указал Таб Аткинс в комментарии на Github. Кроме того, в сообщении в почтовой рассылке рабочей группы CSS (www-style) Таб указал, что -- вот-вот добавят в спеку.

P.S. Это тоже может быть интересно:

6 Комментарии

  1. faiwer

    Я в первую очередь вижу в них удобную возможность встраивания datauri-ресурсов. Дело в том, что CSS не позволяет определить словарь, к примеру, изображений, из которых потом цеплять нужны в list-style-image, background-image, multiple background-image и пр. свойствах. А дублировать такие ресурсы попросту нельзя. Приходилось объединять CSS-селекторы, что чревато некоторыми проблемами. Более того невозможно объединить селектора из разных @media групп. В общем совсем плохо. Теперь же можно составить словарь и черпать изображения уже из него.

    1. SelenIT (Автор записи)

      Отличная идея!

  2. sasha beep

    На мой взгляд, называть константы переменными в препроцессорах было неправильно.
    В том же хелпаке LESS написано

    Note that variables are actually «constants» in that they can only be defined once.

    1. SelenIT (Автор записи)

      А как быть, например, со счетчиками циклов?

  3. Алексей

    Большое спасибо за статью. Зацеплюсь за раздел «Итого»
    Когда я открыл для себя препроцессоры, был очень рад однозначной трансляции их конструкций в понятный всем браузерам css.
    Меня не может не насторожить такой дополнительный уровень абстракции языка, реализация которого привязана к движку браузера.
    Сейчас, на примере js, из проекта в проект кочуют полифилы, потому что могут.
    Что будет кочевать для css?)

  4. SelenIT (Автор записи)

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

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Получать новые комментарии по электронной почте. Вы можете подписаться без комментирования.