这是一篇关于 Shoelace 的文章,这是一个由 Cory LaViska 创建的组件库,但它与众不同。它定义了你所有标准的 UX 组件:选项卡、模态框、手风琴、自动完成以及 更多更多。它们开箱即用,外观精美,易于访问,并且完全可定制。但是,它不是在 React、Solid、Svelte 等中创建这些组件,而是使用 网页组件 创建它们;这意味着你可以将它们与任何框架一起使用。
一些预备事项
网页组件很棒,但目前有一些小问题需要注意。
React
我之前说过它们可以在任何 JavaScript 框架中使用,但正如我之前写的那样,React 对网页组件的支持 目前很差。为了解决这个问题,Shoelace 实际上 创建了包装器,专门用于 React。
另一个我个人喜欢的选项是创建一个薄薄的 React 组件,它接受网页组件的标签名称及其所有属性,然后完成处理 React 缺陷的脏活。我在 之前的一篇文章中 谈到了这个选项。我喜欢这个解决方案,因为它被设计为可以删除。网页组件互操作性问题目前在 React 的实验分支中已修复,因此一旦发布,你使用的任何薄网页组件互操作性组件都可以被搜索和删除,从而留下直接的网页组件使用,而无需任何 React 包装器。
服务器端渲染 (SSR)
在撰写本文时,对 SSR 的支持也很差。理论上,有一种叫做 声明式阴影 DOM (DSD) 的东西,它可以实现 SSR。但浏览器支持很有限,而且无论如何,DSD 实际上需要服务器支持才能正常工作,这意味着 Next、Remix 或你在服务器上使用的任何东西都需要具备一些特殊处理能力。
也就是说,还有其他方法可以让网页组件与使用 Next 等进行 SSR 的 Web 应用程序正常工作
。简而言之,注册网页组件的脚本需要在解析标记之前在阻塞脚本中运行。但这是另一篇文章的主题。
当然,如果你正在构建任何类型的客户端渲染 SPA,这将不是问题。这将是我们在这篇文章中要使用的内容。
让我们开始
由于我希望这篇文章重点关注 Shoelace 及其网页组件特性,我将使用 Svelte 来完成所有操作。我还将使用这个 Stackblitz 项目 进行演示。我们将一起逐步构建此演示,但随时可以打开该 REPL 以查看最终结果。
我将向你展示如何使用 Shoelace,更重要的是,如何自定义它。我们将讨论 阴影 DOM 以及它们阻止来自外部世界哪些样式(以及哪些样式不会阻止)。我们还将讨论 ::part
CSS 选择器 - 你可能完全不了解它 - 我们甚至会看看 Shoelace 如何允许我们覆盖和自定义其各种动画。
如果你在阅读完这篇文章后发现你喜欢 Shoelace 并且想在 React 项目中试用它,我的建议是使用 包装器,就像我在介绍中提到的那样。这将允许你使用 Shoelace 的任何组件,并且一旦 React 发布了他们已经拥有的网页组件修复(在版本 19 中查找),它可以完全被删除。
介绍 Shoelace
Shoelace 有相当详细的 安装说明。最简单的方法是将 <script>
和 <style>
标签放到你的 HTML 文档中,就这样。不过,对于任何生产应用程序,你可能都需要有选择地仅导入所需的内容,也有相应的说明。
安装好 Shoelace 后,让我们创建一个 Svelte 组件来渲染一些内容,然后逐步完成完全自定义它的步骤。为了选择一些相当不平凡的东西,我选择了选项卡和对话框(通常称为模态框)组件。以下是一些从文档中借鉴的标记
<sl-tab-group>
<sl-tab slot="nav" panel="general">General</sl-tab>
<sl-tab slot="nav" panel="custom">Custom</sl-tab>
<sl-tab slot="nav" panel="advanced">Advanced</sl-tab>
<sl-tab slot="nav" panel="disabled" disabled>Disabled</sl-tab>
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
<sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
<sl-tab-panel name="advanced">This is the advanced tab panel.</sl-tab-panel>
<sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel>
</sl-tab-group>
<sl-dialog no-header label="Dialog">
Hello World!
<button slot="footer" variant="primary">Close</button>
</sl-dialog>
<br />
<button>Open Dialog</button>
这将渲染一些漂亮且经过样式化的选项卡。活动选项卡上的下划线甚至会以动画方式很好地显示,并在活动选项卡之间平滑滑动。

我不会浪费你的时间来详细介绍 Shoelace 网站上已经记录得很好的每个 API 的细节。相反,让我们看看如何最好地与这些网页组件交互并完全自定义它们。
与 API 交互:方法和事件
调用网页组件上的方法和订阅事件可能与你习惯使用的普通框架略有不同,但并不复杂。让我们看看怎么做。
选项卡
选项卡组件 (<sl-tab-group>
) 有一个 show
方法,它可以手动显示特定选项卡。为了调用它,我们需要访问选项卡的底层 DOM 元素。在 Svelte 中,这意味着使用 bind:this
。在 React 中,它将是一个 ref
。等等。由于我们使用的是 Svelte,让我们为我们的 tabs
实例声明一个变量
<script>
let tabs;
</script>
... 并绑定它
<sl-tab-group bind:this="{tabs}"></sl-tab-group>
现在我们可以添加一个按钮来调用它
<button on:click={() => tabs.show("custom")}>Show custom</button>
对于事件来说也是一样的。有一个 sl-tab-show
事件,它在显示新选项卡时触发。我们可以对我们的 tabs
变量使用 addEventListener
,或者可以使用 Svelte 的 on:event-name
快捷方式。
<sl-tab-group bind:this={tabs} on:sl-tab-show={e => console.log(e)}>
这可以工作,并在你显示不同的选项卡时记录事件对象。

通常,我们会渲染选项卡并让用户在它们之间单击,因此这项工作通常甚至不需要,但如果你需要它,它就在那里。现在让我们让对话框组件交互起来。
对话框
对话框组件 (<sl-dialog>
) 接受一个 open
属性,它控制对话框是否... 打开。让我们在我们的 Svelte 组件中声明它
<script>
let tabs;
let open = false;
</script>
它还有一个 sl-hide
事件,用于隐藏对话框。让我们传递我们的 open
属性并绑定到 hide
事件,这样我们就可以在用户单击对话框内容外部将其关闭时将其重置。并且让我们添加一个单击处理程序到该关闭按钮,以将我们的 open
属性设置为 false
,这也会关闭对话框。
<sl-dialog no-header {open} label="Dialog" on:sl-hide={() => open = false}>
Hello World!
<button slot="footer" variant="primary" on:click={() => open = false}>Close</button>
</sl-dialog>
最后,让我们连接我们的打开对话框按钮
<button on:click={() => (open = true)}>Open Dialog</button>
就这样。与组件库的 API 交互或多或少是直截了当的。如果这篇文章只是做到了这些,那就太无聊了。
但 Shoelace 采用 Web Components 构建,这意味着某些东西(尤其是样式)的工作方式与我们习惯的方式略有不同。
自定义所有样式!
截至撰写本文时,Shoelace 仍处于测试阶段,创建者正在考虑更改某些默认样式,甚至可能完全删除某些默认样式,以便它们不再覆盖您的主机应用程序的样式。我们将介绍的概念无论哪种方式都适用,但不要惊讶,当您开始使用它时,我提到的某些 Shoelace 特定内容可能有所不同。
Shoelace 的默认样式虽然很棒,但我们可能在 Web 应用中拥有自己的设计,并且希望我们的 UX 组件与之匹配。让我们看看如何在 Web Components 世界中做到这一点。
我们不会试图实际改进任何东西。Shoelace 的创建者比我更出色。相反,我们只看看如何更改事物,以便您可以适应自己的 Web 应用。
快速浏览 Shadow DOMs
查看您的 DevTools 中的其中一个选项卡标题;它应该看起来像这样

我们的选项卡元素创建了一个带有 .tab
和 .tab--active
类的 div
容器,以及一个 tabindex
,同时还显示了我们为此选项卡输入的文本。但请注意,它位于阴影根内。这使 Web Component 作者可以向 Web Component 添加自己的标记,同时还提供一个位置来放置我们提供的 content。注意到 <slot>
元素了吗?这基本上意味着“将用户在 Web Component 标记之间呈现的任何 content 放置在此处。”
因此,<sl-tab>
组件创建一个阴影根,向其中添加一些内容以呈现样式精美的选项卡标题以及一个占位符 (<slot>
),该占位符在其中呈现我们的 content。
封装的样式
Web 开发中一直存在一个经典的、更令人沮丧的问题,即样式级联到我们不希望它们出现的地方。您可能担心应用程序中任何指定类似 div.tab
的样式规则都会干扰这些选项卡。事实证明,这不是问题;阴影根封装了样式。来自阴影根外部的样式不会影响阴影根内部的内容(有一些例外,我们将在后面讨论),反之亦然。
这方面的例外是可继承的样式。当然,您不需要为 Web 应用中的每个元素都应用 font-family
样式。相反,您可以在 :root
或 html
上指定 font-family
一次,并让它继承到下面的所有位置。这种继承实际上也会穿透阴影根。
CSS 自定义属性(通常称为“css 变量”)是一个相关的例外。阴影根绝对可以读取在阴影根外部定义的 CSS 属性;这将在稍后变得相关。
::part
选择器
那么那些不继承的样式呢?如果我们想自定义像 cursor
这样的东西,它不会继承,在阴影根内部。我们运气不佳吗?事实证明,我们没有。再看看上面的选项卡元素图像及其阴影根。注意到 div
上的 part
属性了吗?这使您可以使用 ::part
选择器 从阴影根外部定位和设置该元素的样式。我们将在稍后逐步介绍一个示例。
覆盖 Shoelace 样式
让我们看看这些方法的实际应用。到目前为止,许多 Shoelace 样式(包括字体)都从 CSS 自定义属性接收默认值。要将这些字体与应用程序的样式对齐,请覆盖相关的自定义属性。有关 Shoelace 使用哪些 CSS 变量的信息,请参阅文档,或者您也可以直接在 DevTools 中检查任何给定元素的样式。
通过阴影根继承样式
打开 StackBlitz 项目 的 src
目录中的 app.css
文件。在底部的 :root
部分,您应该看到一个 letter-spacing: normal;
声明。由于 letter-spacing
属性是可继承的,因此请尝试设置一个新值,例如 2px
。保存后,所有 content(包括在阴影根中定义的选项卡标题)都将相应调整。

覆盖 Shoelace CSS 变量
<sl-tab-group>
组件读取 --indicator-color
CSS 自定义属性以设置活动选项卡的下划线。我们可以使用一些基本 CSS 覆盖它
sl-tab-group {
--indicator-color: green;
}
就这样,我们现在有了绿色的指示器!

查询部分
在我的当前 Shoelace 版本 (2.0.0-beta.83) 中,任何未禁用的选项卡都具有 pointer
光标。让我们将其更改为活动(选定)选项卡的默认光标。我们已经看到 <sl-tab>
元素在选项卡标题的容器上添加了 part="base"
属性。此外,当前选定的选项卡会收到 active
属性。让我们使用这些事实来定位活动选项卡,并更改光标
sl-tab[active]::part(base) {
cursor: default;
}
就这样!
自定义动画
为了让比喻更加完善,让我们看看 Shoelace 如何让我们自定义动画。Shoelace 使用 Web Animations API,并公开 setDefaultAnimation
API 来控制不同元素如何动画化其各种交互。有关详细信息,请参阅文档,但以下是如何将 Shoelace 的默认对话框动画从向外扩展和向内收缩更改为从顶部动画化,并在隐藏时向下落下的示例。
import { setDefaultAnimation } from "@shoelace-style/shoelace/dist/utilities/animation-registry";
setDefaultAnimation("dialog.show", {
keyframes: [
{ opacity: 0, transform: "translate3d(0px, -20px, 0px)" },
{ opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
],
options: { duration: 250, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});
setDefaultAnimation("dialog.hide", {
keyframes: [
{ opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
{ opacity: 0, transform: "translate3d(0px, 20px, 0px)" },
],
options: { duration: 200, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});
该代码位于 App.svelte
文件中。将其注释掉以查看原始的默认动画。
总结
Shoelace 是一个非常雄心勃勃的组件库,它使用 Web Components 构建。由于 Web Components 与框架无关,因此它们可以在任何项目中使用,无论使用哪种框架。随着新的框架开始出现,它们既具有惊人的性能特征,又易于使用,因此使用高质量的用户体验小部件的能力(这些小部件不绑定到任何特定框架)从未如此引人注目。