Пользовательские свойства CSS в каскаде
Опубликовано: 2022-03-10В прошлом месяце у меня был разговор в Твиттере о разнице между стилями с ограниченной областью действия (генерируемыми в процессе сборки) и «вложенными» стилями, родными для CSS. Я спросил, почему, как ни странно, разработчики избегают специфики селекторов идентификаторов, используя «стили с ограниченной областью действия», созданные JavaScript? Keith Grant предположил, что разница заключается в балансировании каскада* и наследования, т. е. в отдании предпочтения близости специфичности. Давайте взглянем.
Каскад
Каскад CSS основан на трех факторах:
- Важность определяется флагом
!important
и источником стиля (пользователь > автор > браузер). - Специфика используемых селекторов (встроенный > ID > класс > элемент)
- Исходный порядок самого кода (самый последний имеет приоритет)
Близость нигде не упоминается — отношение DOM-дерева между частями селектора. Оба абзаца ниже будут выделены красным цветом, хотя #inner p
описывает более тесную связь, чем #outer p
для второго абзаца:
<section> <p>This text is red</p> <div> <p>This text is also red!</p> </div> </section>
#inner p { color: green; } #outer p { color: red; }
Оба селектора имеют одинаковую специфичность, они оба описывают один и тот же элемент p
, и ни один из них не помечен как !important
, поэтому результат основан только на исходном порядке.
БЭМ и стили области действия
Соглашения об именах, такие как BEM («Block__Element — Modifier»), используются для обеспечения того, чтобы каждый абзац «привязывался» только к одному родителю, полностью избегая каскада. Параграфу «элементы» присваиваются уникальные классы, специфичные для их контекста «блока»:
<section class="outer"> <p class="outer__p">This text is red</p> <div class="inner"> <p class="inner__p">This text is green!</p> </div> </section>
.inner__p { color: green; } .outer__p { color: red; }
Эти селекторы по-прежнему имеют ту же относительную важность, специфичность и исходный порядок, но результаты разные. «Ограниченные» или «модульные» инструменты CSS автоматизируют этот процесс, переписывая для нас наш CSS на основе HTML. В приведенном ниже коде каждый абзац ограничен своим прямым родительским элементом:
<section outer-scope> <p outer-scope>This text is red</p> <div outer-scope inner-scope> <p inner-scope>This text is green!</p> </div> </section>
p[inner-scope] { color: green } p[outer-scope] { color: red; }
Наследование
Близость не является частью каскада, но является частью CSS. Вот где наследование становится важным. Если мы удалим p
из наших селекторов, каждый абзац унаследует цвет от своего ближайшего предка:
#inner { color: green; } #outer { color: red; }
Поскольку #inner
и #outer
описывают разные элементы, наш div
и section
соответственно, оба свойства цвета применяются без конфликтов. Цвет вложенного элемента p
не указан, поэтому результаты определяются наследованием (цветом прямого родителя), а не каскадом . Близость имеет приоритет, а значение #inner
имеет приоритет над значением #outer
.
Но есть проблема: чтобы использовать наследование, мы стилизуем все внутри нашего section
и div
. Мы хотим конкретно настроить цвет абзаца.
(Повторно) введение пользовательских свойств
Пользовательские свойства предоставляют новое решение для браузера; они наследуются, как и любое другое свойство, но их не обязательно использовать там, где они определены . Используя простой CSS, без каких-либо соглашений об именах или инструментов сборки, мы можем создать стиль, который одновременно является целевым и контекстуальным, с близостью, имеющей приоритет над каскадом:
p { color: var(--paragraph); } #inner { --paragraph: green; } #outer { --paragraph: red; }
Пользовательское свойство --paragraph
наследуется точно так же, как свойство color
, но теперь у нас есть контроль над тем, как и где применяется это значение. Свойство --paragraph
действует аналогично параметру, который может быть передан в компонент p
либо посредством прямого выбора (правила специфичности), либо контекста (правила близости).
Я думаю, что это раскрывает потенциал для пользовательских свойств, которые мы часто связываем с функциями, примесями или компонентами.
Пользовательские «функции» и параметры
Функции, примеси и компоненты основаны на одной и той же идее: многократно используемый код, который можно запускать с различными входными параметрами для получения согласованных, но настраиваемых результатов. Разница в том, что они делают с результатами. Мы начнем с переменной полосатого градиента, а затем сможем расширить ее до других форм:
html { --stripes: linear-gradient( to right, powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); }
Эта переменная определена в корневом html
-элементе (можно также использовать :root
, но это добавляет ненужную специфичность), поэтому наша полосатая переменная будет доступна повсюду в документе. Мы можем применить его везде, где поддерживаются градиенты:
body { background-image: var(--stripes); }
Добавление параметров
Функции используются как переменные, но определяют параметры для изменения вывода. Мы можем обновить нашу переменную --stripes
, чтобы она была более похожей на функцию, определив внутри нее некоторые переменные, подобные параметрам. Я начну с замены to right
на var(--stripes-angle)
, чтобы создать параметр, изменяющий угол:
html { --stripes: linear-gradient( var(--stripes-angle), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); }
Есть и другие параметры, которые мы могли бы создать, в зависимости от того, для какой цели предназначена функция. Должны ли мы позволять пользователям выбирать свои собственные цвета полос? Если да, то принимает ли наша функция 5 различных цветовых параметров или только 3, которые будут выходить наружу-внутрь, как сейчас? Хотим ли мы также создать параметры для цветовых остановок? Каждый добавляемый нами параметр обеспечивает больше возможностей настройки за счет простоты и последовательности.
Универсального правильного ответа на этот баланс не существует — некоторые функции должны быть более гибкими, а другие — более самостоятельными. Абстракции существуют для обеспечения согласованности и удобочитаемости вашего кода, поэтому сделайте шаг назад и спросите, каковы ваши цели. Что действительно нужно настраивать, и где должна быть обеспечена согласованность? В некоторых случаях может быть полезнее иметь две самостоятельные функции, а не одну полностью настраиваемую функцию.
Чтобы использовать приведенную выше функцию, нам нужно передать значение параметра --stripes-angle
и применить вывод к выходному свойству CSS, например background-image
:
/* in addition to the code above… */ html { --stripes-angle: 75deg; background-image: var(--stripes); }
Унаследованное против универсального
Я определил функцию --stripes
для элемента html
по привычке. Пользовательские свойства наследуются, и я хочу, чтобы моя функция была доступна везде, поэтому есть смысл поместить ее в корневой элемент. Это хорошо работает для наследования таких переменных, как --brand-color: blue
, поэтому мы можем ожидать, что это сработает и для нашей «функции». Но если мы попытаемся снова использовать эту функцию во вложенном селекторе, она не сработает:
div { --stripes-angle: 90deg; background-image: var(--stripes); }
Новый --stripes-angle
полностью игнорируется. Оказывается, мы не можем полагаться на наследование для функций, которые необходимо пересчитать. Это связано с тем, что каждое значение свойства вычисляется один раз для каждого элемента (в нашем случае — корневого элемента html
), а затем вычисленное значение наследуется . Определяя нашу функцию в корне документа, мы не делаем всю функцию доступной для потомков — только вычисленный результат нашей функции.
Это имеет смысл, если вы создадите его с точки зрения каскадного параметра --stripes-angle
. Как и любое унаследованное свойство CSS, оно доступно потомкам, но не предкам. Значение, которое мы установили для вложенного div
, недоступно для функции, которую мы определили для корневого предка html
. Чтобы создать общедоступную функцию, которая будет пересчитывать любой элемент, мы должны определить ее для каждого элемента:
* { --stripes: linear-gradient( var(--stripes-angle), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); }
Универсальный селектор делает нашу функцию доступной везде, но мы можем определить ее более узко, если захотим. Важно то, что он может пересчитывать только там, где это явно определено. Вот несколько альтернатив:
/* make the function available to elements with a given selector */ .stripes { --stripes: /* etc… */; } /* make the function available to elements nested inside a given selector */ .stripes * { --stripes: /* etc… */; } /* make the function available to siblings following a given selector */ .stripes ~ * { --stripes: /* etc… */; }
Это можно расширить с помощью любой логики селектора, не зависящей от наследования.
Бесплатные параметры и резервные значения
В нашем примере выше var(--stripes-angle)
не имеет значения и резервного варианта. В отличие от переменных Sass или JS, которые должны быть определены или созданы до их вызова, пользовательские свойства CSS можно вызывать, даже не определяя их. Это создает «свободную» переменную, похожую на параметр функции, который может быть унаследован из контекста.
В конечном итоге мы можем определить переменную в html
или :root
(или любом другом предке), чтобы установить унаследованное значение, но сначала нам нужно рассмотреть запасной вариант, если значение не определено. Есть несколько вариантов, в зависимости от того, какое именно поведение мы хотим
- Для «обязательных» параметров нам не нужен запасной вариант. Как есть, функция ничего не будет делать, пока не будет определен
--stripes-angle
. - Для «необязательных» параметров мы можем предоставить резервное значение в функции
var()
. После имени переменной мы добавляем запятую, а затем значение по умолчанию:
var(--stripes-angle, 90deg)
Каждая функция var()
может иметь только один запасной вариант, поэтому любые дополнительные запятые будут частью этого значения. Это позволяет указывать сложные значения по умолчанию с внутренними запятыми:
html { /* Computed: Hevetica, Ariel, sans-serif */ font-family: var(--sans-family, Hevetica, Ariel, sans-serif); /* Computed: 0 -1px 0 white, 0 1px 0 black */ test-shadow: var(--shadow, 0 -1px 0 white, 0 1px 0 black); }
Мы также можем использовать вложенные переменные для создания собственных каскадных правил, присваивая разным значениям разные приоритеты:
var(--stripes-angle, var(--global-default-angle, 90deg))
- Во-первых, попробуйте наш явный параметр (
--stripes-angle
); - Возврат к глобальному «пользовательскому умолчанию» (
--user-default-angle
), если он доступен; - Наконец, вернитесь к нашим «заводским настройкам по умолчанию»
(90deg
).
Установив резервные значения в var()
вместо явного определения пользовательского свойства, мы гарантируем, что для параметра не будет ограничений по специфичности или каскадности. Все параметры *-angle
«свободны» для наследования из любого контекста.
Резервные варианты браузера по сравнению с резервными вариантами переменных
Когда мы используем переменные, нам нужно помнить о двух запасных путях:
- Какое значение следует использовать браузерам без поддержки переменных?
- Какое значение должны использовать браузеры, поддерживающие переменные, когда определенная переменная отсутствует или недействительна?
p { color: blue; color: var(--paragraph); }
В то время как старые браузеры будут игнорировать свойство объявления переменной и вернуться к blue
, современные браузеры будут читать и то и другое и использовать последнее. Наша переменная var(--paragraph)
может быть не определена, но она действительна и переопределит предыдущее свойство, поэтому браузеры с поддержкой переменных будут возвращаться к унаследованному или начальному значению, как если бы они использовали ключевое слово unset
.
Сначала это может показаться запутанным, но на это есть веские причины. Первый — технический: браузерные движки обрабатывают недопустимый или неизвестный синтаксис во время «анализа» (что происходит первым), но переменные не разрешаются до «времени вычисления значения» (что происходит позже).
- Во время синтаксического анализа объявления с недопустимым синтаксисом полностью игнорируются, возвращаясь к более ранним объявлениям. Это путь, по которому пойдут старые браузеры. Современные браузеры поддерживают синтаксис переменных, поэтому предыдущее объявление отбрасывается.
- Во время вычисления значения переменная компилируется как недопустимая, но уже слишком поздно — предыдущее объявление уже было отброшено. Согласно спецификации, недопустимые значения переменных обрабатываются так же, как и
unset
:
html { color: red; /* ignored as *invalid syntax* by all browsers */ /* - old browsers: red */ /* - new browsers: red */ color: not a valid color; color: var(not a valid variable name); /* ignored as *invalid syntax* by browsers without var support */ /* valid syntax, but invalid *values* in modern browsers */ /* - old browsers: red */ /* - new browsers: unset (black) */ --invalid-value: not a valid color value; color: var(--undefined-variable); color: var(--invalid-value); }
Это также хорошо для нас как авторов, потому что позволяет нам экспериментировать с более сложными резервными вариантами для браузеров, поддерживающих переменные, и предоставлять простые резервные варианты для старых браузеров. Более того, это позволяет нам использовать состояние null
/ undefined
для установки необходимых параметров. Это становится особенно важным, если мы хотим превратить функцию в миксин или компонент.
Пользовательское свойство «Миксины»
В Sass функции возвращают необработанные значения, в то время как примеси обычно возвращают фактический вывод CSS с парами свойство-значение. Когда мы определяем универсальное свойство --stripes
, не применяя его ни к какому визуальному выводу, результат подобен функции. Мы можем сделать так, чтобы он вел себя как миксин, также определив вывод универсально:
* { --stripes: linear-gradient( var(--stripes-angle), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); background-image: var(--stripes); }
Пока --stripes-angle
остается недействительным или неопределенным, миксин не скомпилируется, и background-image
не будет применено. Если мы установим допустимый угол для любого элемента, функция вычислит и даст нам фон:
div { --stripes-angle: 30deg; /* generates the background */ }
К сожалению, это значение параметра будет унаследовано, поэтому текущее определение создает фон для div
и всех потомков . Чтобы исправить это, мы должны убедиться, что значение --stripes-angle
не наследуется, установив его в initial
(или любое недопустимое значение) для каждого элемента. Мы можем сделать это на том же универсальном селекторе:
* { --stripes-angle: initial; --stripes: /* etc… */; background-image: var(--stripes); }
Безопасные встроенные стили
В некоторых случаях нам нужно, чтобы параметр задавался динамически извне CSS — на основе данных с внутреннего сервера или внешнего фреймворка. С помощью настраиваемых свойств мы можем безопасно определять переменные в нашем HTML, не беспокоясь об обычных проблемах специфичности:
<div>...</div>
Встроенные стили имеют высокую специфичность, и их очень сложно переопределить, но с пользовательскими свойствами у нас есть еще один вариант: игнорировать их. Если мы установим для div значение background-image: none
(например), эта встроенная переменная не окажет никакого влияния. Чтобы пойти еще дальше, мы можем создать промежуточную переменную:
* { --stripes-angle: var(--stripes-angle-dynamic, initial); }
Теперь у нас есть возможность определить --stripes-angle-dynamic
в HTML или проигнорировать его и установить --stripes-angle
непосредственно в нашей таблице стилей.
Предустановленные значения
Для более сложных значений или общих шаблонов, которые мы хотим использовать повторно, мы также можем предоставить несколько предустановленных переменных на выбор:
* { --tilt-down: 6deg; --tilt-up: -6deg; }
И используйте эти пресеты, а не устанавливайте значение напрямую:
<div>...</div>
Это отлично подходит для создания диаграмм и графиков на основе динамических данных или даже для планирования дня.
Контекстные компоненты
Мы также можем преобразовать наш «примесь» в «компонент», применив его к явному селектору и сделав параметры необязательными. Вместо того, чтобы полагаться на наличие или отсутствие --stripes-angle
для переключения нашего вывода, мы можем полагаться на наличие или отсутствие селектора компонентов. Это позволяет нам безопасно устанавливать резервные значения:
[data-stripes] { --stripes: linear-gradient( var(--stripes-angle, to right), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); background-image: var(--stripes); }
Поместив запасной вариант внутрь функции var()
, мы можем оставить --stripes-angle
неопределенным и «свободным» для наследования значения извне компонента. Это отличный способ представить определенные аспекты стиля компонента для контекстного ввода. Даже стили с ограниченной областью действия, сгенерированные фреймворком JS (или ограниченные областью действия внутри теневой модели DOM, например значки SVG), могут использовать этот подход для предоставления определенных параметров внешнему влиянию.
Изолированные компоненты
Если мы не хотим предоставлять параметр для наследования, мы можем определить переменную со значением по умолчанию:
[data-stripes] { --stripes-angle: to right; --stripes: linear-gradient( var(--stripes-angle, to right), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); background-image: var(--stripes); }
Эти компоненты также будут работать с классом или любым другим допустимым селектором, но я выбрал атрибут data-
, чтобы создать пространство имен для любых модификаторов, которые нам нужны:
[data-stripes='vertical'] { --stripes-angle: to bottom; } [data-stripes='horizontal'] { --stripes-angle: to right; } [data-stripes='corners'] { --stripes-angle: to bottom right; }
Селекторы и параметры
Мне часто хотелось бы использовать атрибуты данных для установки переменной — функция, поддерживаемая спецификацией CSS3 attr()
, но еще не реализованная ни в одном браузере (см. вкладку ресурсов для связанных проблем в каждом браузере). Это позволило бы нам более тесно связать селектор с конкретным параметром:
<div data-stripes="30deg">...</div> /* Part of the CSS3 spec, but not yet supported */ /* attr( , ) */ [data-stripes] { --stripes-angle: attr(data-stripes angle, to right); }
<div data-stripes="30deg">...</div> /* Part of the CSS3 spec, but not yet supported */ /* attr( , ) */ [data-stripes] { --stripes-angle: attr(data-stripes angle, to right); }
<div data-stripes="30deg">...</div> /* Part of the CSS3 spec, but not yet supported */ /* attr( , ) */ [data-stripes] { --stripes-angle: attr(data-stripes angle, to right); }
Тем временем мы можем добиться чего-то подобного, используя атрибут style
:
<div>...</div> /* The `*=` atttribute selector will match a string anywhere in the attribute */ [style*='--stripes-angle'] { /* Only define the function where we want to call it */ --stripes: linear-gradient(…); }
Этот подход наиболее полезен, когда мы хотим включить другие свойства в дополнение к устанавливаемому параметру. Например, установка области сетки может также добавить отступы и фон:
[style*='--grid-area'] { background-color: white; grid-area: var(--grid-area, auto / 1 / auto / -1); padding: 1em; }
Заключение
Когда мы начинаем собирать все эти части воедино, становится ясно, что пользовательские свойства выходят далеко за рамки привычных нам вариантов использования переменных. Мы можем не только хранить значения и привязывать их к каскаду, но и использовать их для управления каскадом по-новому и создавать более интеллектуальные компоненты непосредственно в CSS.
Это требует от нас переосмысления многих инструментов, на которые мы полагались в прошлом — от соглашений об именах, таких как SMACSS и BEM, до стилей с ограниченной областью действия и CSS-in-JS. Многие из этих инструментов помогают обойти специфику или управлять динамическими стилями на другом языке — варианты использования, которые теперь мы можем решать напрямую с помощью пользовательских свойств. Динамические стили, которые мы часто рассчитывали в JS, теперь можно обрабатывать, передавая необработанные данные в CSS.
Поначалу эти изменения могут восприниматься как «дополнительная сложность» — поскольку мы не привыкли видеть логику внутри CSS. И, как и в случае со всем кодом, чрезмерная разработка может быть реальной опасностью. Но я бы сказал, что во многих случаях мы можем использовать эту силу не для усложнения , а для того, чтобы переместить сложность из сторонних инструментов и соглашений обратно в основной язык веб-дизайна и (что более важно) обратно в язык. браузер. Если наши стили требуют вычислений, эти вычисления должны находиться внутри нашего CSS.
Все эти идеи можно развить гораздо дальше. Пользовательские свойства только начинают получать более широкое распространение, а мы только начали исследовать возможности. Я взволнован, чтобы видеть, куда это идет, и что еще люди придумывают. Повеселись!
Дальнейшее чтение
- «Пришло время начать использовать пользовательские свойства CSS», Серг Хосподарец
- «Стратегическое руководство по пользовательским свойствам CSS», Майкл Ритмюллер.