CSS 和样式设置
本文介绍了使用 Shadow DOM 可以实现的更多精彩功能。 它基于 Shadow DOM 101 中讨论的概念构建而成。如果需要简介,请参阅该文章。
简介
毫无疑问,未设置样式的标记没有任何问题。幸运的是,Web 组件背后的杰出人才预见了这一点,并且没有让我们挂断。CSS 作用域模块定义了许多用于在影子树中设置内容样式的选项。
样式封装
Shadow DOM 的核心功能之一是阴影边界。它有很多不错的属性,但最棒的是,它免费提供样式封装。换个说法:
<div><h3>Light DOM</h3></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = `
<style>
h3 {
color: red;
}
</style>
<h3>Shadow DOM</h3>
`;
</script>
关于此演示,有两个有趣的观察结果:
- 此页面上还有其他 h3,但只有 ShadowRoot 中的那个 h3 与 h3 选择器匹配(因此样式为红色)。同样,默认为作用域样式。
- 此页面上定义的其他针对 h3 的样式规则不会渗透到我的内容中。 这是因为,选择器不会跨越阴影边界。
这个故事是否具有寓意?我们对外来世界进行了样式封装。感谢 Shadow DOM!
设置主元素的样式
:host
可让您选择托管影子树的元素并为其设置样式:
<button class="red">My Button</button>
<script>
var button = document.querySelector('button');
var root = button.createShadowRoot();
root.innerHTML = `
<style>
:host {
text-transform: uppercase;
}
</style>
<content></content>
`;
</script>
一个问题是,父页面中规则的特异性高于在元素中定义的 :host
规则,但比在宿主元素上定义的 style
属性低。这可让用户从外部替换您的样式。:host
也仅适用于 ShadowRoot 的上下文,因此您无法在 Shadow DOM 之外使用它。
:host(<selector>)
函数形式允许您定位与 <selector>
匹配的主元素。
示例 - 仅当元素本身具有类 .different
(例如 <x-foo class="different"></x-foo>
)时匹配:
:host(.different) {
...
}
响应用户状态
:host
的一个常见用例是,如果您要创建自定义元素并希望对不同的用户状态(:hover、:focus、:active 等)作出响应,
<style>
:host {
opacity: 0.4;
transition: opacity 420ms ease-in-out;
}
:host(:hover) {
opacity: 1;
}
:host(:active) {
position: relative;
top: 3px;
left: 3px;
}
</style>
为元素设置主题
如果 :host-context(<selector>)
伪类与宿主元素匹配,如果该元素或其任何祖先实体与 <selector>
匹配,则该伪类与该元素匹配。
:host-context()
的一个常见用途是根据元素的周围环境为其设置主题。例如,许多人通过将类应用于 <html>
或 <body>
来进行主题设置:
<body class="different">
<x-foo></x-foo>
</body>
当 <x-foo>
是某个具有 .different
类的元素的后代时,您可以使用 :host-context(.different)
来设置其样式:
:host-context(.different) {
color: red;
}
这样,您就可以在元素的 Shadow DOM 中封装样式规则,以便根据其上下文为其设置独特的样式。
在一个影子根目录中支持多种主机类型
如果您要创建主题库,并希望支持在同一 Shadow DOM 内设置多种类型的宿主元素,:host
的另一个用途就是支持这些元素。
:host(x-foo) {
/* Applies if the host is a <x-foo> element.*/
}
:host(x-foo:host) {
/* Same as above. Applies if the host is a <x-foo> element. */
}
:host(div) {
/* Applies if the host element is a <div>. */
}
从外部设置 Shadow DOM 内部项的样式
::shadow
伪元素和 /deep/
组合器就像是一把 Vorpal 之剑的 CSS 授权。它们允许穿透 Shadow DOM 的边界,对影子树内的元素进行样式设置。
::shadow 伪元素
如果某个元素至少有一个影子树,则 ::shadow
伪元素会与影子根本身匹配。它允许您编写选择器,用于设置元素阴影圆顶的内部节点的样式。
例如,如果某个元素托管了一个影子根,您可以编写 #host::shadow span {}
来设置其影子树中的所有 span 的样式。
<style>
#host::shadow span {
color: red;
}
</style>
<div id="host">
<span>Light DOM</span>
</div>
<script>
var host = document.querySelector('div');
var root = host.createShadowRoot();
root.innerHTML = `
<span>Shadow DOM</span>
<content></content>
`;
</script>
示例(自定义元素)- <x-tabs>
在其 Shadow DOM 中有 <x-panel>
子元素。每个面板都有自己的影子树,其中包含 h2
标题。若要从主页面设置这些标题的样式,可以编写:
x-tabs::shadow x-panel::shadow h2 {
...
}
/deep/ 组合符
/deep/
组合器与 ::shadow
类似,但功能更强大。它会完全忽略所有阴影边界,并进入任意数量的阴影树。简而言之,/deep/
可让您深入了解元素的内部并定位任何节点。
/deep/
组合器在自定义元素(通常具有多层 Shadow DOM)中特别有用。Prime 示例是嵌套多个自定义元素(每个元素都托管各自的影子树),或使用 <shadow>
创建从其他元素继承的元素。
示例(自定义元素)- 选择树中任意位置为 <x-tabs>
后代的所有 <x-panel>
元素:
x-tabs /deep/ x-panel {
...
}
示例 - 使用类 .library-theme
设置影子树中任何位置的所有元素样式:
body /deep/ .library-theme {
...
}
使用 querySelector()
就像 .shadowRoot
为 DOM 遍历打开影子树一样,组合器会打开影子树以进行选择器遍历。您可以编写一个语句,而不是编写一个嵌套的疯狂链:
// No fun.
document.querySelector('x-tabs').shadowRoot
.querySelector('x-panel').shadowRoot
.querySelector('#foo');
// Fun.
document.querySelector('x-tabs::shadow x-panel::shadow #foo');
设置原生元素的样式
原生 HTML 控件在设置样式方面带来挑战。很多人都直接放弃,自己掷骰子选人。不过,借助 ::shadow
和 /deep/
,您可以为网络平台中使用 Shadow DOM 的任何元素设置样式。<input>
类型和 <video>
就是很好的例子:
video /deep/ input[type="range"] {
background: hotpink;
}
创建样式钩子
自定义很好。在某些情况下,您可能需要在 Shadow 的样式盾牌上戳个孔,并创建钩子以供他人进行样式设置。
使用 ::shadow 和 /deep/
/deep/
的背后蕴含着巨大的力量。它为组件作者提供了一种方式,能够将单个元素指定为可设置样式的元素,或将一系列元素指定为可主题化元素。
示例 - 为具有 .library-theme
类的所有元素设置样式,忽略所有影子树:
body /deep/ .library-theme {
...
}
使用自定义伪元素
WebKit 和 Firefox 都定义了用于设置原生浏览器元素内部片段的样式的伪元素。input[type=range]
就是一个很好的例子。您可以通过定位 ::-webkit-slider-thumb
来设置滑块滑块 <span style="color:blue">blue</span>
的样式:
input[type=range].custom::-webkit-slider-thumb {
-webkit-appearance: none;
background-color: blue;
width: 10px;
height: 40px;
}
与浏览器为某些内部元素提供样式钩子的方式类似,Shadow DOM 内容的作者可以将某些元素指定为可由外部人员设置样式。这是通过自定义伪元素实现的。
您可以使用 pseudo
属性将某个元素指定为自定义伪元素。它的值(或名称)需要带有“x-”前缀。这样做会创建与影子树中该元素的关联,并为外部用户提供一条指定的通道来穿过影子边界。
以下示例展示了如何创建自定义滑块 widget 并允许用户将其滑块滑块样式设为蓝色:
<style>
#host::x-slider-thumb {
background-color: blue;
}
</style>
<div id="host"></div>
<script>
var root = document.querySelector('#host').createShadowRoot();
root.innerHTML = `
<div>
<div pseudo="x-slider-thumb"></div>' +
</div>
`;
</script>
使用 CSS 变量
CSS 变量是创建主题钩子的有效方式。从本质上讲,就是创建“样式占位符”以供其他用户填充。
假设一位自定义元素作者在其 Shadow DOM 中标注了变量占位符。一个用于设置内部按钮的字体样式,另一个用于设置按钮的颜色:
button {
color: var(--button-text-color, pink); /* default color will be pink */
font-family: var(--button-font);
}
然后,元素的嵌入器会根据自己的喜好定义这些值。或许与自己页面超酷的 Comic Sans 主题相匹配:
#host {
--button-text-color: green;
--button-font: "Comic Sans MS", "Comic Sans", cursive;
}
由于 CSS 变量的继承方式,一切都采用桃红色,并且效果非常好!整个过程如下所示:
<style>
#host {
--button-text-color: green;
--button-font: "Comic Sans MS", "Comic Sans", cursive;
}
</style>
<div id="host">Host node</div>
<script>
var root = document.querySelector('#host').createShadowRoot();
root.innerHTML = `
<style>
button {
color: var(--button-text-color, pink);
font-family: var(--button-font);
}
</style>
<content></content>
`;
</script>
重置样式
字体、颜色和行高等可继承样式会继续影响 Shadow DOM 中的元素。不过,为了最大限度地提高灵活性,Shadow DOM 提供了 resetStyleInheritance
属性来控制在阴影边界处发生的情况。您可以将其视为一种在创建新组件时从头开始应用的一种方法。
resetStyleInheritance
false
- 默认值。可继承的 CSS 属性会继续继承。true
- 在边界处将可继承属性重置为initial
。
以下演示显示了更改 resetStyleInheritance
如何影响影子树:
<div>
<h3>Light DOM</h3>
</div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.resetStyleInheritance = <span id="code-resetStyleInheritance">false</span>;
root.innerHTML = `
<style>
h3 {
color: red;
}
</style>
<h3>Shadow DOM</h3>
<content select="h3"></content>
`;
</script>
<div class="demoarea" style="width:225px;">
<div id="style-ex-inheritance"><h3 class="border">Light DOM</div>
</div>
<div id="inherit-buttons">
<button id="demo-resetStyleInheritance">resetStyleInheritance=false</button>
</div>
<script>
var container = document.querySelector('#style-ex-inheritance');
var root = container.createShadowRoot();
//root.resetStyleInheritance = false;
root.innerHTML = '<style>h3{ color: red; }</style><h3>Shadow DOM<content select="h3"></content>';
document.querySelector('#demo-resetStyleInheritance').addEventListener('click', function(e) {
root.resetStyleInheritance = !root.resetStyleInheritance;
e.target.textContent = 'resetStyleInheritance=' + root.resetStyleInheritance;
document.querySelector('#code-resetStyleInheritance').textContent = root.resetStyleInheritance;
});
</script>
了解 .resetStyleInheritance
比较复杂,主要是因为它只影响可继承的 CSS 属性。它会说明:当您查找要继承的属性时,在页面和 ShadowRoot 之间的边界,不从主机继承值,而是使用 initial
值(根据 CSS 规范)。
如果您不确定要沿用 CSS 中的哪些属性,请查看此便捷列表,或在“元素”面板中取消选中“显示继承项”复选框。
设置分布式节点的样式
分布式节点是在插入点处渲染的元素(<content>
元素)。<content>
元素可让您从 Light DOM 中选择节点,并在 Shadow DOM 中的预定义位置进行渲染。从逻辑上讲,它们不在 Shadow DOM 内;它们仍是主元素的子元素。插入点只是用来呈现内容。
分布式节点会保留主文档的样式。也就是说,即使元素在插入点呈现,主页面中的样式规则也会继续应用于这些元素。同样,分布式节点在逻辑上仍位于光亮区中,不会移动。它们只是呈现在其他位置。不过,当节点分布到 Shadow DOM 中时,它们可以采用在影子树内定义的其他样式。
::content 伪元素
分布式节点是主机元素的子项,那么我们如何从 Shadow DOM 内部定位它们呢?答案是 CSS ::content
伪元素。
这是一种定位穿过插入点的 Light DOM 节点的方法。例如:
::content > h3
设置通过插入点的所有 h3
标记的样式。
我们来看一个示例:
<div>
<h3>Light DOM</h3>
<section>
<div>I'm not underlined</div>
<p>I'm underlined in Shadow DOM!</p>
</section>
</div>
<script>
var div = document.querySelector('div');
var root = div.createShadowRoot();
root.innerHTML = `
<style>
h3 { color: red; }
content[select="h3"]::content > h3 {
color: green;
}
::content section p {
text-decoration: underline;
}
</style>
<h3>Shadow DOM</h3>
<content select="h3"></content>
<content select="section"></content>
`;
</script>
重置插入点的样式
创建 ShadowRoot 时,您可以选择重置继承的样式。<content>
和 <shadow>
插入点也支持此选项。使用这些元素时,请在 JS 中设置 .resetStyleInheritance
,或对元素本身使用布尔值 reset-style-inheritance
属性。
对于 ShadowRoot 或
<shadow>
插入点:reset-style-inheritance
表示在可继承的 CSS 属性触及影子内容之前,在主机上将其设为initial
。此位置称为上限。对于
<content>
插入点:reset-style-inheritance
表示先将可继承的 CSS 属性设为initial
,然后再将主机的子项分发到该插入点。此位置称为下边界。
总结
作为自定义元素的作者,我们有大量选项来控制内容的外观和风格。Shadow DOM 奠定了这个勇敢新世界的基础。
Shadow DOM 为我们提供了作用域样式封装,并提供了一种方式来允许尽可能多(或尽可能少)传入我们选择的外部世界。通过定义自定义伪元素或添加 CSS 变量占位符,作者可以提供第三方便捷的样式钩子,用于进一步自定义其内容。总的来说,网络作者可以完全控制其内容的呈现方式。