Web Components #6 组件样式css
自定义组件样式
web组件的css注意项.主要是 Shadow DOM的样式,和module中加载css. css in js.
ShadowDOM 样式特点
- 封装性:Shadow DOM 允许开发者将自定义的样式和行为封装在一个独立的作用域中,与全局样式和其他组件的样式相互隔离,避免样式冲突和污染。
- 作用域限定:Shadow DOM 中的样式只适用于其内部的元素,不会影响外部的 DOM 结构。这使得开发者可以创建独立的、可复用的组件,而不会干扰全局样式或其他组件的样式。
- 继承性:Shadow DOM 元素可以继承其父元素的样式。通过使用
inherit
关键字,可以让 Shadow DOM 元素继承父元素的样式属性,从而实现样式的继承和重用。 - 插槽样式:当使用
<slot>
元素在 Shadow DOM 中插入内容时,可以为插槽内容设置样式。这样可以在 Shadow DOM 内部控制插槽内容的样式,实现更灵活的样式定制。 - CSS 变量:Shadow DOM 支持使用 CSS 变量来定义样式,并通过 JavaScript 动态修改这些变量的值。这样可以实现更灵活的样式定制和主题切换。
- 样式优先级:Shadow DOM 的样式遵循 CSS 的级联规则,具有一定的优先级。内联样式具有最高优先级,其次是 Shadow DOM 内部的样式表,然后是外部的样式表。这样可以确保 Shadow DOM 的样式可以被外部样式所覆盖,实现更灵活的样式定制。
样式隔离
<body>
<style>
.content{color:red;}
</style>
<div class="content">外部.content</div>
<my-card></my-card>
<template id="card-template">
<div class="content">
内部.content
</div>
</template>
<script>
class MyCard extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
const template=document.querySelector('#card-template');
shadowRoot.appendChild(template.content.cloneNode(true));
}
}
customElements.define('my-card', MyCard);
</script>
</body>
- 外部样式影响不到内部.
- 内部样式影响不到外部
<div class="content">外部.content</div>
<template id="card-template">
<style>
.content{color:green;}
</style>
<div class="content">内部.content</div>
</template>
<div class="content">外部.content</div>
继承样式
和普通元素一样继承#app的样式.默认是all:inherit
<style>
#app {color:red;font-size:24px;}
</style>
<div id="app">
<div class="content">外部.content</div>
<my-card></my-card>
</div>
<template id="card-template">
<div class="content">内部.content</div>
</template>
改成initial,重置所有样式.
<template id="card-template">
<style>
:host{all:initial;}
</style>
<div class="content">内部.content</div>
</template>
slot样式
组件可以定义slot的样式,外部可以定义slot的内容样式.
slot 并不会移动 light DOM,light DOM 节点分布到 shadow DOM 中后,slot 会对其 DOM 进行渲染,样式设置,但是节点实际还是留在原处。如果外部对 light DOM 设置了样式,那么外部样式将会覆盖 shadow DOM 中通过 ::slotted(<compound-selector>)
设置的样式,具有较高优先级。
- 组件内定义slot样式
slot里的样式默认继承父级样式.
<div id="app">
<div class="content">外部.content</div>
<my-card></my-card>
</div>
<template id="card-template">
<style>
:host{all:inherit}
.main{color:green;}
</style>
<div class="content">
内部.content<br />
<slot class="main">默认slot</slot>
</div>
</template>
- 组件外定义slot插入的内容
<p>
本身就是在外部,可以随时定义样式.
<style>
#app {color:red;font-size:24px;}
#app p{color:blue;}/* 定义slot的内容的样式 */
</style>
<div id="app">
<div class="content">外部.content</div>
<my-card>
<p>插入内容</p>
</my-card>
</div>
<template id="card-template">
<style>
:host{all:inherit}
.main{color:green;}
</style>
<div class="content">
内部.content<br />
<slot class="main">默认slot</slot>
</div>
</template>
::slotted
插槽插入内容后的样式.
::slotted(p)
选择插槽内容里的p标签,不支持textNode节点.
<my-card></my-card>
<my-card><p>有内容</p></my-card>
<template id="card-template">
<style>
:host{all:inherit}
/* 默认样式 */
.main{color:green;}
/* 插入内容的样式 */
.main::slotted(p){color:orange;}
</style>
<div class="content">
内部.content<br />
<slot class="main"><p>默认slot</p></slot>
</div>
</template>
外部修改组件样式的方式
组件是样式隔离的,在组件外部,我们不能直接通过选择器对组件内的元素设置样式.
注: 外部样式总是优先于在 shadow DOM 中定义的样式
可以通过几种方式修改: 全局css变量修改,::part
,,js修改
尝试直接修改.
<style>
my-card header{color:red;}
my-card content{color:blue;}
my-card footer{color:green;}
</style>
<div id="app">
<my-card></my-card>
</div>
<template id="card-template">
<style>
</style>
<div class="content">
<header>卡片标题</header>
<content>卡片内容</content>
<footer>卡片底部</footer>
</div>
</template>
结果,无法修改.
通过css变量修改
<style>
:root{
--header-color:red;
--content-color:blue;
--footer-color:green;
}
</style>
<div id="app">
<my-card></my-card>
</div>
<template id="card-template">
<style>
header{color:var(--header-color,gray);}
content{color:var(--content-color,gray);}
footer{color:var(--footer-color,gray);}
</style>
<div class="content">
<header>卡片标题</header>
<content>卡片内容</content>
<footer>卡片底部</footer>
</div>
</template>
结果,外部可以直接修改.
也可以把变量局部到组件内.
<style>
my-card{
--header-color:red;
--content-color:blue;
--footer-color:green;
}
</style>
<div id="app">
<my-card></my-card>
</div>
<template id="card-template">
<style>
:host{
--header-color:black;
--content-color:black;
--footer-color:black;
}
header{color:var(--header-color,gray);}
content{color:var(--content-color,gray);}
footer{color:var(--footer-color,gray);}
</style>
<div class="content">
<header>卡片标题</header>
<content>卡片内容</content>
<footer>卡片底部</footer>
</div>
</template>
通过::part
外部可以通过::part
修改组件部分.
注意::part不是选择器. 不能使用类似 ::part(header) p {}
<style>
#card1::part(header) {
color: red;
}
#card1::part(content) {
color: blue;
}
#card1::part(footer) {
color: green;
}
</style>
<div id="app">
<my-card id="card1"></my-card>
</div>
<template id="card-template">
<style>
:host::part(header) {
font-size: 20px;
}
</style>
<div class="content">
<header part="header">卡片标题</header>
<content part="content">卡片内容</content>
<footer part="footer">卡片底部</footer>
</div>
</template>
通过JS来改变内联样式
- 直接修改样式.
const card1=document.querySelector('#card1');
card1.shadowRoot.querySelector('header').style.color="red"
- 添加样式表:
const card1=document.querySelector('#card1');
const styles = new CSSStyleSheet();
styles.replaceSync(`
header{color:red;font-size:20px;}
`)
card1.shadowRoot.adoptedStyleSheets=[styles];
组件内部影响外部
组件是互相隔离.
只能通过JS修改外部样式.
引入css
和html一样.
- 内联样式
- 外部样式表
内联样式
style标签,style标签
<script>
customElements.define("my-card",
class MyCard extends HTMLElement{
constructor(){
super();
this
.attachShadow({mode:"open"})
.innerHTML=`
<style>
:host{
display: block;
background:red;
}
</style>
<div style="color:green">组件</div>
`
}
})
</script>
或通过document.createElement("style")
外部样式表
<script>
customElements.define("my-card",
class MyCard extends HTMLElement{
constructor(){
super();
this
.attachShadow({mode:"open"})
.innerHTML=`
<link
rel="stylesheet"
href="style.css" />
<div style="color:green">组件</div>
`
}
})
</script>
adoptedStyleSheets 动态加载
通过动态创建CSSStyleSheet
class MyCard extends HTMLElement{
constructor(){
super();
this
.attachShadow({mode:"open"})
.innerHTML=`<div style="color:green">组件</div>`
const styles=new CSSStyleSheet();
styles.replaceSync(`
:host{background:red;display:block;}
`)
styles.insertRule(`div{font-size:22px;}`);
this.shadowRoot.adoptedStyleSheets=[styles];
}
}
module js 中引用css模块.
通过adoptedStyleSheets 更容易打包.
import styles from "./style.css?inline" assert { type: "css" };
export default class MyCard extends HTMLElement{
constructor(){
super();
this
.attachShadow({mode:"open"})
.innerHTML=`
<div style="color:green">组件</div>
`
this.shadowRoot.adoptedStyleSheets=[styles];
}
}
customElements.define('my-card', MyCard);
使用
<script type="module" src="card.js"></script>
类似vue把css写在js中.
function css(strings, ...values) {
return tring.raw(strings, ...values);
}
class XFoo extends HTMLElement {
static styles=css`
:host{color:red;font-size:${10+10}px;}
`
constructor() {
super()
const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(this.constructor.styles);
this.shadowRoot.adoptedStyleSheets=[styleSheet];
}
}
customElements.define('x-foo', XFoo)
ShadowDom组件常用的css选择器
:host
: 当前根标签
<my-card >
<p>Hello World</p>
</my-card>
<template>
<style>
:host{
display:block;
border:1px solid gray;
}
</style>
</template>
:host()
: 匹配当前根组件,但只能根元素,不能与其他后代选择同时使用.
<my-card theme="dark">
<p>Hello World</p>
</my-card>
<template>
<style>
:host([theme="dark"]){
background-color:#444;
color:#fff;
}
/* 不支持 */
:host([theme="dark"]) p{
//...
}
</style>
</template>
:host-context(xxx)
: 匹配当前根组件,与:host()
类似. 但支持后代选择器
<my-card theme="dark">
<p>Hello World</p>
</my-card>
<template>
<style>
:host-content([theme="dark"]){
background-color:#444;
color:#fff;
}
/* 支持 */
:host-content([theme="dark"]) p{
color: blue;
}
</style>
</template>
::slotted(xxx)
,已分配的根slot插槽元素.不支持后代选择,不支持TextNode选择.
<my-card >
<p class="section">支持</p>
<p class="section">支持</p>
<div>
<p class="section">不支持</p>
</div>
</my-card>
<template>
<style>
::slotted(.section){
color:red;
}
/* 不支持 */
::slotted(div) p{
color:green;
}
/* 支持 */
::slotted(div)::before{
content: "before";
}
</style>
</template>
::part(xxx)
: 对外暴露的部分,类似class. 不支持后代选择器.
<template>
<header part="title"><h3>弹窗</h3></header>
<content part="message">message</content>
<footer>
<button part="confim-btn">确定</button>
<button part="close-btn">关闭</button>
</footer>
<style>
/* 组件里面使用 */
:host::part(title){
font-weight: bold;
}
</style>
</template>
组件外部
<my-dialog></my-dialog>
<style>
my-dialog::part(close-btn){
color:red;
}
/* 不支持 */
my-dialog::part(title) h3{
color:red;
}
</style>
:focus-within
,自己域子元素获得焦点时.
<template shadowrootmode="open">
<style>
:host{
display: block;
border: 1px solid #ccc;
padding: 20px;
width:120px;
}
:host(:focus-within){
background-color: red;
}
</style>
<div>
<input type="text" />
</div>
</template>
:defined
,表示任何已定义的元素。这包括任何浏览器内置的标准元素以及已成功定义的自定义元素注意包含标准元素如div. 也包含TextNode
常用于解决自定义组件FOUC(无样式内容的闪现).
由于自定义组件通常是用 customElements.define
声明的.js加载,渲染有延时. 此时自定义标签当成简单html标签处理. web组件加载完成后样式会变化,就产生闪烁. 我们通常的解决办法是,先把标签display:none
,组件渲染完成后再去掉none
.
<style>
my-delay:not(:defined){
display: none;
}
</style>
<my-delay><p>不显示</p></my-delay>
<my-delay><p>不显示</p></my-delay>
<my-delay><p>不显示</p></my-delay>
<script>
class MyDelay extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
}
connectedCallback() {
this.shadowRoot.innerHTML="显示 "
}
}
// 延迟注册组件
setTimeout(function(){
customElements.define('my-delay', MyDelay);
},1000)
// undefined
console.log(customElements.get('my-delay'));
</script>
TextNode我们经常忽略.
下面示例,虽然<unkown-com>
是一个未知的标签,但是里面包含TextNode节点,文字一样变红色.
<style>
:defined{
color:red;
}
</style>
<unkown-com>
未定义组件
</unkown-com>
一般配合:not()
使用.
<style>
:not(:defined){
display:none;
}
</style>
<unkown-com>
未定义组件
</unkown-com>
原作者:阿金
本文地址:https://hi-arkin.com/archives/web-components-5.html