Теневая модель документа: 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>
var root = document.querySelector('div').webkitCreateShadowRoot();
root.innerHTML = '<style>h3{ color: red; }</style>' + 
                 '<h3>Заголовок, принадлежащий теневому дереву</h3>';
</script>

Заголовок, принадлежащий ведущему элементу

По поводу этого демо есть два интересных замечания:

В чём мораль? Мы получаем инкапсуляцию стилей от внешнего мира. Спасибо, теневая модель документа!

Стилизация элемента 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>

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

Наследование и обнуление стилей

В некоторых случаях нужен доступ внутрь теневого дерева для посторонних стилей. Самый яркий пример — это виджет для комментирования. Большинство разработчиков, встраивая такой виджет в код, наверняка хотят, чтобы он органично вписывался в их страницу. Я бы точно этого хотел. Следовательно, нам нужен способ, который позволил бы повторить внешний вид и настроение страницы, на которую устанавливается виджет, посредством наследования шрифтов, цвета, высоты строки и т.д.

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

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

<div><h3>Заголовок, принадлежащий ведущему элементу</h3></div>
<script>
var root = document.querySelector('div').webkitCreateShadowRoot();
root.applyAuthorStyles = true;
root.resetStyleInheritance = false;
root.innerHTML = '<style>h3{ color: red; }</style>' + 
                 '<h3>Заголовок, принадлежащий теневому дереву</h3>' + 
                 '<content select="h3"></content>';
</script>

Заголовок, принадлежащий ведущему элементу

Понять, как работает .applyAuthorStyles просто. Он заставляет элементы h3 теневого дерева наследовать внешний вид других элементов h3, который определен для страницы (т.е. применяются стили разработчика страницы).

Даже при настроенном атрибуте apply-author-styles, CSS-селекторы, описанные в коде страницы, не пересекают теневую границу. Стилевые правила согласовываются только когда они полностью помещены внутрь или вне теневого дерева.

Унаследованные свойства в DevTools

.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 для самого элемента.

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

Заключение

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

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


Примечания

1 Для этого вам нужно использовать Google Chrome и активировать «Отображать теневую модель документа (Show Shadow DOM)» в инструментах разработчика.