slot 插槽
slot 全局属性用于在 Web Component 中为 Shadow DOM 的插槽(Slot)分配内容。通过 slot 属性,使用者可以将自定义内容插入到组件的特定位置,实现灵活的组件组合和内容分发。
前置知识
阅读本节前,建议先了解:part 与 exportparts
基础概念
什么是插槽
插槽(Slot)是 Web Components 中 Shadow DOM 的内容分发机制。组件定义者使用 <slot> 元素在 Shadow DOM 中声明"占位符",使用者通过 slot 属性将内容分发到对应的占位符中。
html
<!-- 组件定义:Shadow DOM 中的 slot 占位符 -->
<template id="my-card">
<div class="card">
<div class="header"><slot name="title">默认标题</slot></div>
<div class="body"><slot>默认内容</slot></div>
<div class="footer"><slot name="actions">默认操作</slot></div>
</div>
</template>
<!-- 组件使用:通过 slot 属性分发内容 -->
<my-card>
<span slot="title">自定义标题</span>
<p>自定义内容</p>
<button slot="actions">确认</button>
</my-card>具名插槽与默认插槽
| 插槽类型 | 组件定义 | 使用方式 | 说明 |
|---|---|---|---|
| 默认插槽 | <slot> | 直接放入内容 | 不需要 slot 属性 |
| 具名插槽 | <slot name="xxx"> | <span slot="xxx"> | 需要匹配 name 和 slot 属性 |
语法
定义插槽
html
<!-- 默认插槽 -->
<slot>默认内容(当没有分发内容时显示)</slot>
<!-- 具名插槽 -->
<slot name="header"></slot>
<slot name="body"></slot>
<slot name="footer"></slot>使用插槽
html
<!-- 默认插槽:直接放入内容 -->
<my-component>
<p>这会放入默认插槽</p>
</my-component>
<!-- 具名插槽:使用 slot 属性匹配 -->
<my-component>
<h2 slot="header">标题</h2>
<p>这会放入默认插槽</p>
<button slot="footer">操作</button>
</my-component>详细说明
插槽机制原理
当浏览器渲染 Web Component 时,插槽的分发过程如下:
- 收集组件标签内的所有子元素("轻量 DOM"或 Light DOM)
- 根据
slot属性值,将子元素与 Shadow DOM 中的<slot>元素匹配 - 未匹配的子元素分配给默认
<slot>(如果存在) - 渲染时,被分配的元素"投影"到对应插槽的位置
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>插槽分发机制</title>
</head>
<body>
<script>
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.card {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
width: 300px;
}
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
background: #e2e8f0;
}
.info { margin-top: 12px; }
.name { font-weight: 600; font-size: 18px; }
.bio { color: #64748b; margin-top: 4px; }
.actions { margin-top: 12px; display: flex; gap: 8px; }
</style>
<div class="card">
<slot name="avatar">
<!-- 默认头像 -->
<div class="avatar"></div>
</slot>
<div class="info">
<div class="name"><slot name="name">匿名用户</slot></div>
<div class="bio"><slot name="bio">这个人很懒,什么都没写。</slot></div>
</div>
<div class="actions">
<slot name="actions">
<button>关注</button>
</slot>
</div>
</div>
`;
}
}
customElements.define('user-card', UserCard);
</script>
<!-- 完整使用 -->
<user-card>
<img slot="avatar" src="avatar.jpg" alt="用户头像" width="64" height="64">
<span slot="name">张三</span>
<span slot="bio">前端开发工程师,热爱开源。</span>
<div slot="actions">
<button>关注</button>
<button>发消息</button>
</div>
</user-card>
<!-- 部分使用(其余显示默认内容) -->
<user-card>
<span slot="name">李四</span>
</user-card>
</body>
</html>多个元素分配到同一插槽
多个元素可以使用相同的 slot 属性值,它们都会被分发到同一个插槽中:
html
<!-- 组件定义 -->
<template id="icon-group">
<style>
.icons { display: flex; gap: 8px; }
</style>
<div class="icons">
<slot name="icon"></slot>
</div>
</template>
<!-- 使用:多个元素共享同一个具名插槽 -->
<icon-group>
<img slot="icon" src="star.svg" alt="收藏">
<img slot="icon" src="heart.svg" alt="喜欢">
<img slot="icon" src="share.svg" alt="分享">
</icon-group>插槽的样式化
通过 ::slotted() 伪元素,组件作者可以为被分发到插槽中的内容定义样式:
javascript
class StyledCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
/* ::slotted() 选择被分发到插槽的顶层元素 */
::slotted(h2) {
font-size: 20px;
color: #1e293b;
margin: 0;
}
::slotted(p) {
color: #64748b;
line-height: 1.6;
}
::slotted([slot="actions"]) {
display: flex;
gap: 8px;
}
/* 只能选择直接子元素,不能深入选择 */
/* ::slotted(.inner-class) 如果 .inner-class 不是顶层元素则无效 */
</style>
<div class="card">
<slot name="header"></slot>
<slot></slot>
<slot name="actions"></slot>
</div>
`;
}
}插槽事件与 Slotchange
slotchange 事件在插槽内容发生变化时触发:
javascript
class SlotWatcher extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<div class="container">
<slot name="items"></slot>
</div>
`;
// 监听插槽内容变化
this.shadowRoot.querySelector('slot[name="items"]')
.addEventListener('slotchange', (e) => {
const items = e.target.assignedElements();
console.log('插槽内容已更新,共', items.length, '个元素');
// 对每个分发元素进行处理
items.forEach(item => {
item.classList.add('slot-item');
});
});
}
}
customElements.define('slot-watcher', SlotWatcher);实战示例
完整的选项卡组件
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Slot 属性实战:选项卡组件</title>
<style>
body { font-family: system-ui; padding: 40px; }
.demo { margin-bottom: 40px; }
</style>
</head>
<body>
<script>
class TabPanel extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
}
.tab-bar {
display: flex;
border-bottom: 1px solid #e2e8f0;
background: #f8fafc;
}
.tab-btn {
padding: 10px 20px;
border: none;
background: transparent;
cursor: pointer;
font-size: 14px;
color: #64748b;
transition: all 0.2s;
border-bottom: 2px solid transparent;
}
.tab-btn[aria-selected="true"] {
color: #3b82f6;
border-bottom-color: #3b82f6;
background: white;
}
.tab-content {
padding: 20px;
}
/* 通过 ::slotted() 样式化分发内容 */
::slotted(h3) {
margin: 0 0 8px;
font-size: 16px;
}
::slotted(p) {
color: #475569;
line-height: 1.6;
margin: 0;
}
</style>
<div class="tab-bar" part="tab-bar" role="tablist"></div>
<div class="tab-content">
<slot></slot>
</div>
`;
this._tabs = [];
this._activeIndex = 0;
}
connectedCallback() {
// 收集所有带 slot 属性的 tab-panel 元素
this._tabs = Array.from(this.querySelectorAll('[slot^="panel-"]'));
// 创建标签按钮
const tabBar = this.shadowRoot.querySelector('.tab-bar');
this._tabs.forEach((panel, index) => {
const btn = document.createElement('button');
btn.className = 'tab-btn';
btn.role = 'tab';
btn.textContent = panel.dataset.label || `标签 ${index + 1}`;
btn.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
btn.addEventListener('click', () => this.switchTab(index));
tabBar.appendChild(btn);
});
// 初始隐藏非激活面板
this._tabs.forEach((panel, index) => {
panel.style.display = index === 0 ? 'block' : 'none';
});
}
switchTab(index) {
this._activeIndex = index;
const buttons = this.shadowRoot.querySelectorAll('.tab-btn');
buttons.forEach((btn, i) => {
btn.setAttribute('aria-selected', i === index ? 'true' : 'false');
});
this._tabs.forEach((panel, i) => {
panel.style.display = i === index ? 'block' : 'none';
});
}
}
customElements.define('tab-panel', TabPanel);
</script>
<div class="demo">
<h2>使用 slot 属性的选项卡</h2>
<tab-panel>
<div slot="panel-0" data-label="概述">
<h3>产品概述</h3>
<p>这是一个功能强大的选项卡组件,使用 Web Components 和 slot 属性实现。</p>
</div>
<div slot="panel-1" data-label="特性">
<h3>核心特性</h3>
<p>支持任意内容、自定义标签名称、键盘导航和无障碍访问。</p>
</div>
<div slot="panel-2" data-label="文档">
<h3>使用文档</h3>
<p>使用 slot="panel-N" 属性定义面板内容,通过 data-label 属性设置标签名。</p>
</div>
</tab-panel>
</div>
</body>
</html>注意事项
::slotted() 的限制
css
/* ::slotted() 只能选择直接子元素 */
::slotted(p) { /* 有效:p 是顶层被分发元素 */ }
/* ::slotted() 不能选择嵌套元素 */
/* ::slotted(p span) { 无效 } */
/* ::slotted() 不能与后代选择器组合 */
/* ::slotted(p).active { 无效,但 ::slotted(p.active) 有效 } */slot 属性只在组件使用时有效
html
<!-- 在普通 HTML 中,slot 属性无意义 -->
<div slot="title">这不会做任何事情</div>
<!-- slot 属性只在 Web Component 的 light DOM 中有效 -->
<my-component>
<div slot="title">这里有效</div>
</my-component>最佳实践
1. 为所有插槽提供默认内容
html
<!-- 推荐:每个插槽都有默认内容 -->
<slot name="header"><h3>默认标题</h3></slot>
<slot><p>默认内容区域</p></slot>
<slot name="footer"><button>确定</button></slot>
<!-- 避免:空插槽,使用者必须提供内容 -->
<slot name="header"></slot>2. 语义化 slot 命名
html
<!-- 推荐:使用语义化名称 -->
<slot name="title">标题</slot>
<slot name="description">描述</slot>
<slot name="avatar">头像</slot>
<slot name="actions">操作</slot>
<!-- 避免:使用数字或无意义名称 -->
<slot name="slot1">标题</slot>
<slot name="a">描述</slot>
<slot name="xxx">头像</slot>下一节
继续学习:popover 弹出层