Теневая модель документа: CSS и стили
Для того, что бы примеры работали, смотрите статью в Google Chrome и включите в настройках, доступных по адресу chrome://flags/#enable-experimental-web-platform-features опцию Enable experimental Web Platform features.
В этой статье продолжается описание удивительных возможностей теневой модели документа (Shadow DOM). Оно опирается на концепции, которые были описаны в первой статье о теневой модели документа. Прочитайте её, если хотите ознакомиться с основами.
Введение
Взглянем правде в лицо. Без стилей разметка выглядит совершенно не привлекательно. К счастью для нас, ребята, которые придумали веб-компоненты, это предусмотрели и не оставили нас на произвол судьбы. Существует множество способов стилизовать контент в теневом дереве.
Инкапсуляция стилей
Одним из ключевых компонентов теневой модели документа является граница теневого дерева (shadow boundary). Она обладает целым набором интересных свойств, однако одно из лучших — это обеспечение инкапсуляции стилей без дополнительных усилий с нашей стороны. Другими словами:
По умолчанию CSS-стили, описанные внутри теневого дерева, ограничены его корневым элементом.
Ниже приведён пример. Если всё пошло так, как надо, и ваш браузер поддерживает теневую модель документа1, вы увидите «Заголовок, принадлежащий теневому дереву».
<div><h3>Заголовок, принадлежащий ведущему элементу</h3></div>
<script></script>
Заголовок, принадлежащий ведущему элементу
По поводу этого демо есть два интересных замечания:
- На этой странице есть и другие теги
h3
, но селектору с условиемh3
соответствует только тот, что помещён в корневой элемент теневого дерева, который, соответственно, окрашен в красный цвет. - Другие стилевые правила для элементов
h3
, определённые для этой страницы, не влияют на контент внутри теневого дерева, потому что селекторы не применяются
к элементам находящимся в теневом дереве.
В чём мораль? Мы получаем инкапсуляцию стилей от внешнего мира. Спасибо, теневая модель документа!
Стилизация элемента host
@-правило @host позволяет выбрать и стилизовать элемент, который содержит теневое дерево:
<button class="bigger">Моя кнопка</button>
<script>
var root = document.querySelector('button').webkitCreateShadowRoot();
root.innerHTML = '<style>' +
'@host{' +
'button { text-transform: uppercase; }' +
'.bigger { padding: 20px; }' +
'}' +
'</style>' +
'<content select=""></content>';
</script>
Здесь трюк в том, что селекторы внутри @host
имеют большую
специфичность, чем любой селектор на родительской странице, но меньшую, чем
инлайновые стили, определённые для ведущего элемента. Кроме того, @host
работает только в контексте корневого элемента теневого дерева, и не может быть
использован за его пределами.
@host
можно использовать для создания настраиваемого элемента, который должен
реагировать на различные действия пользователя (:hover
, :focus
, :active
, и
т.д.).
<style>
@host {
* {
opacity: 0.4;
+transition: opacity 420ms ease-in-out;
}
*:hover {
opacity: 1;
}
*:active {
position: relative;
top: 3px;
left: 3px;
}
}
</style>
В этом примере я использовал «*», чтобы обратиться к любому элементу теневого дерева. «Мне без разницы, что ты за элемент, используй эти стили.»
Также @host
может пригодиться при стилизации нескольких ведущих элементов в
одном теневом дереве, скажем, когда вы создаёте настраиваемый элемент. Или,
например, когда у вас есть несколько вариантов оформления, привязанных к одному
ведущему элементу.
@host {
g-foo {
/* Применяется, если ведущий элемент является элементом <g-foo> */
}
g-bar {
/* Применяется, если ведущий элемент является элементом <g-bar> */
}
div {
/* Применяется, если ведущий элемент является элементом <div>. */
}
* {
/* Применяется к элементу любого типа, который является ведущим для
данного корневого элемента теневого дерева. */
}
}
Применение стилей извне теневого дерева
Кастомизация — это хорошо. В некоторых случаях может возникнуть необходимость оставить зазоры в защите стилизации теневого дерева и сделать возможным добавление для него дополнительных стилей.
Использование настраиваемых псевдоэлементов
И у WebKit, и у Firefox определены псевдоэлементы, которые
используются для стилизации внутренних компонентов нативных элементов браузера.
Хорошим примером является input[type=range]
. Ползунок слайдера можно сделать
синим, если прописать соответствующие правила для ::-webkit-slider-thumb
:
input[type=range].custom::-webkit-slider-thumb {
-webkit-appearance: none;
background-color: blue;
width: 10px;
height: 40px;
}
Так же, как разработчики браузеров предоставляют возможность указания стилей
для внутренних компонентов браузеров, авторы контента в теневом дереве могут
выделить некоторые элементы, стиль которых может быть изменён извне. Это
делается с помощью настраиваемых псевдоэлементов.
2
Обозначить элемент как настраиваемый псевдоэлемент можно с помощью атрибута
pseudo
. Его значение, или же имя, должно содержать префикс «x-». Это создаёт
привязку к соответствующему элементу в теневом дереве и оставляет
лазейку для пересечения границы теневого дерева.
Вот пример создания настраиваемого виджета-слайдера с возможностью изменения цвета ползунка на синий:
<style>
#host::x-slider-thumb {
background-color: blue;
}
</style>
<div id="host"></div>
<script>
var root = document.querySelector('#host').webkitCreateShadowRoot();
root.innerHTML = '<div>' +
'<div pseudo="x-slider-thumb"></div>' +
'</div>';
</script>
А знаете, почему настраиваемые псевдоэлементы действительно крутые? К ним можно применять стили с помощью внешнего CSS, но на них нельзя воздействовать с помощью внешнего JavaScript. Граница теневого дерева непреодолима для JavaScript, но предусматривает послабление для описания характеристик настраиваемых псевдоэлементов.
Использование переменных в CSS
Поддержку переменных в CSS можно активировать в Chrome в разделе
«Экспериментальные функции» на странице about:flags
.
Переменные в CSS — это ещё один эффективный способ управления стилями. По сути это создание своеобразных «стилевых плейсхолдеров», содержимое которых может быть изменено посторонними.
Возможный сценарий использования: разработчик настраиваемого элемента обозначает переменные-плейсхолдеры в теневом дереве. Одну для стилизации шрифта кнопки виджета, а другую — для изменения его цвета:
button {
color: +var (button-text-color, pink); /* по умолчанию применён розовый цвет */
font: +var (button-font) ;
}
Впоследствии тот, кто вставляет этот элемент к себе в разметку, может настроить эти значения по своему усмотрению. Например, чтобы кнопка соответствовала шикарному оформлению страницы с использованием шрифта Comic Sans:
#host {
+var-button-text-color: green;
+var-button-font: "Comic Sans MS", "Comic Sans", cursive;
}
Благодаря тому, как происходит наследование для переменных в CSS, всё работает и выглядит просто изумительно! Вот картина целиком:
<style>
#host {
+var-button-text-color: green;
+var-button-font: "Comic Sans MS", "Comic Sans", cursive;
}
</style>
<div id="host">Ведущий узел</div>
<script>
var root = document.querySelector('#host').webkitCreateShadowRoot();
root.innerHTML = '<style>' +
'button {' +
'color: +var (button-text-color, pink);' +
'font: +var (button-font) ;' +
'}' +
'</style>' +
'<content></content>';
</script>
Я уже несколько раз упомянул настраиваемые элементы в этой статье. Я не буду о них здесь рассказывать. Пока что достаточно будет информации о том, что теневая модель документа служит для них структурной основой, и концепции стилизации, описанные в этой статье, имеют отношение к настраиваемым элементам.
Наследование и обнуление стилей
В некоторых случаях нужен доступ внутрь теневого дерева для посторонних стилей. Самый яркий пример — это виджет для комментирования. Большинство разработчиков, встраивая такой виджет в код, наверняка хотят, чтобы он органично вписывался в их страницу. Я бы точно этого хотел. Следовательно, нам нужен способ, который позволил бы повторить внешний вид и настроение страницы, на которую устанавливается виджет, посредством наследования шрифтов, цвета, высоты строки и т.д.
В целях повышения гибкости настройки, в защите стилей теневого дерева оставлены ещё несколько лазеек. Есть два свойства, с помощью которых можно управлять стилизацией теневого дерева:
.resetStyleInheritance
false
— значение по умолчанию. CSS-свойства, которым свойственно наследование, продолжают наследовать стиль родительских элементов.true
— обнуляет значения CSS-свойств до исходных на теневой границе.
.applyAuthorStyles
true
— применяются стили, описанные в основном коде страницы. Можно рассматривать это как разрешение стилям «просачиваться» сквозь границу.false
— значение по умолчанию. Стили страницы не применяются для теневого дерева.
Ниже представлено демо, показывающее, как изменение этих двух свойств влияет на теневое дерево.
<div><h3>Заголовок, принадлежащий ведущему элементу</h3></div>
<script></script>
Заголовок, принадлежащий ведущему элементу
Понять, как работает .applyAuthorStyles
просто. Он заставляет элементы h3
теневого дерева наследовать внешний вид других элементов h3
, который определен
для страницы (т.е. применяются стили разработчика страницы).
Даже при настроенном атрибуте
apply-author-styles
, CSS-селекторы, описанные в коде страницы, не пересекают теневую границу. Стилевые правила согласовываются только когда они полностью помещены внутрь или вне теневого дерева.
.resetStyleInheritance
немного сложнее для понимания, в первую очередь потому,
что оно действует только на те CSS-свойства, которые могут наследовать
родительские значения. Оно говорит: «проверяя на границе между кодом страницы и
корневым элементом теневого дерева наличие родительского свойства, которое нужно
унаследовать, свойство в теневом дереве не должно наследовать свойства страницы,
вместо этого следует использовать исходное значение initial
(согласно
спецификации CSS)».
Если вы не уверены в том, какие свойства наследуют родительские значения в CSS, взгляните на этот удобный список или поставьте галочку напротив «Показать унаследованные свойства» («Show inherited») в разделе «Element» панели разработчика.
Шпаргалка по применению свойств
Чтобы помочь вам разобраться, когда применять эти свойства, ниже представлена матрица решений. Держите её под рукой. Она на вес золота!
Ситуация | applyAuthorStyles | resetStyleInheritance |
---|---|---|
«В общем внешний вид у меня свой, но базовые свойства вроде цвета текста должны быть такими же, как у страницы.» Попросту говоря, вы создаетё виджет |
false | false |
«Забудьте о стилях страницы! У меня своё оформление.» Вам всё же потребуется «обнуление стилей компонента», так как совместный контент сохраняет стили, которые у него были на странице. |
false | true |
«Я компонент, который должен унаследовать внешний вид страницы.» | true | true |
«Я хочу влиться в страницу насколько это возможно.» Помните, что селекторы не действуют по другую сторону теневой границы. |
true | false |
Стилизация передаваемых в теневое дерево элементов
.applyAuthorStyles
и .resetStyleInheritance
предназначены строго для управления
стилями узлов, заданных в теневом дереве.
С элементами, передаваемыми в теневое дерево, всё иначе. Согласно логике они не принадлежат теневому дереву, они — дочерние элементы ведущего узла, которые вставляются на место в «момент отображения». Закономерно, они принимают стиль страницы, на которой находятся (страницы ведущего элемента). Единственное исключение в этом правиле — они могут принимать дополнительные стилевые характеристики контекста, в который вставляются (теневого дерева).
Псевдоэлемент ::distributed()
Если узлы, передаваемые в теневое дерево, являются дочерними для ведущего
элемента, как тогда можно к ним обратиться и стилизовать изнутри теневого
дерева? Правильный ответ — используя псевдоэлемент ::distributed()
. Это первый
функциональный псевдоэлемент, который принимает CSS-селектор в качестве
параметра.
Взглянем на простой пример:
<div><p>Заголовок, принадлежащий ведущему элементу</p></div>
<script>
var root = document.querySelector('div').webkitCreateShadowRoot();
root.innerHTML = '<style>' +
'p{ color: red; }' +
'content::-webkit-distributed(p) { color: green; }' +
'</style>' +
'<p>Заголовок, принадлежащий теневому дереву</p>' +
'<content select="p"></content>';
</script>
Заголовок, принадлежащий ведущему элементу
Вы должны под ним увидеть «Заголовок, принадлежащий теневому дереву» и «Заголовок, принадлежащий ведущему элементу». Также обратите внимание что «Заголовок, принадлежащий ведущему элементу» сохраняет стили страницы.
Обнуление стилей в точке вставки
Создавая корневой элемент теневого дерева, можно обнулить унаследованные стили.
Точки вставки через <content>
и <shadow>
также дают такую возможность.
Пропишите .resetStyleInheritance
в JavaScript, используя эти элементы, или
примените логический атрибут reset-style-inheritance
для самого элемента.
- Для корневого элемента теневого дерева или точки вставки через
<shadow>
:reset-style-inheritance
обнуляет значения CSS-свойств, которые могут наследовать родительские значения, до значений по умолчанию на этапе ведущего элемента, до взаимодействия с контентом теневого дерева. Этот промежуток известен как верхняя теневая граница. - Для точки вставки через
<content>
:reset-style-inheritance
обнуляет значения CSS-свойств, которые могут наследовать родительские значения, до значений по умолчанию до того, как дочерние элементы ведущего элемента переданы в теневое дерево. Этот промежуток известен как нижняя теневая граница.
Помните: стили, прописанные в основном коде продолжают применяться к узлам, на которые они нацелены, даже когда эти узлы передаются внутрь теневого дерева. Переход через точку вставки не меняет того, что применяется.
Заключение
У нас, разработчиков настраиваемых элементов, есть масса возможностей управлять внешним видом контента и тем, какое впечатление он производит. Теневая модель документа составляет основу этого дивного нового мира.
Теневая модель документа даёт нам возможность инкапсулировать стили и передавать внутрь теневого дерева ровно столько внешнего воздействия, сколько нам нужно. Определив настраиваемые псевдоэлементы или добавив переменные-плейсхолдеры, разработчики могут предоставить удобную возможность управлять стилями с целью настройки контента. В целом, веб-разработчики получают полный контроль над представлением контента.
Примечания
1 Для этого вам нужно использовать Google Chrome и активировать «Отображать теневую модель документа (Show Shadow DOM)» в инструментах разработчика.