This is a post about șiret de pantofi, a component library by Cory LaViska, but with a twist. It defines all your standard UX components: tabs, modals, accordions, auto-completes, and mult, mult mai mult. They look beautiful out of the box, are accessible, and fully customizable. But rather than creating these components in React, or Solid, or Svelte, etc., it creates them with Componente web; this means you can use them with Orice cadru.
Some preliminary things
Web Components are great, but there’s currently a few small hitches to be aware of.
Reacţiona
I said they work in any JavaScript framework, but as I’ve written before, React’s support for Web Components is currently poor. To address this, Shoelace actually created wrappers just for React.
Another option, which I personally like, is to create a thin React component that accepts the tag name of a Web Component and all of its attributes and properties, then does the dirty work of handling React’s shortcomings. I talked about this option într-un post anterior. I like this solution because it’s designed to be deleted. The Web Component interoperability problem is currently fixed in React’s experimental branch, so once that’s shipped, any thin Web Component-interoperable component you’re using could be searched, and removed, leaving you with direct Web Component usages, without any React wrappers.
Redare pe partea serverului (SSR)
Support for SSR is also poor at the time of this writing. In theory, there’s something called Declarative Shadow DOM (DSD) which would enable SSR. But browser support is minimal, and in any event, DSD actually requires suport pentru server to work right, which means Pagina Următoare →, Remix, or whatever you happen to use on the server will need to become capable of some special handling.
That said, there are other ways to get Web Components to doar munca
with a web app that’s SSR’d with something like Next. The short version is that the scripts registering your Web Components need to run in a blocking script before your markup is parsed. But that’s a topic for another post.
Of course, if you’re building any kind of client-rendered SPA, this is a non-issue. This is what we’ll work with in this post.
Să începem
Since I want this post to focus on Shoelace and on its Web Component nature, I’ll be using Svelt for everything. I’ll also be using this Stackblitz project for demonstration. We’ll build this demo together, step-by-step, but feel free to open that REPL up anytime to see the end result.
I’ll show you how to use Shoelace, and more importantly, how to customize it. We’ll talk about DOM-uri în umbră and which styles they block from the outside world (as well as which ones they don’t). We’ll also talk about the ::part
CSS selector — which may be entirely new to you — and we’ll even see how Shoelace allows us to override and customize its various animations.
If you find you like Shoelace after reading this post and want to try it in a React project, my advice is to use a wrapper like I mentioned in the introduction. This will allow you to use any of Shoelace’s components, and it can be removed altogether once React ships the Web Component fixes they already have (look for that in version 19).
Introducing Shoelace
Shoelace has fairly detailed instructiuni de instalare. At its most simple, you can dump și
tags into your HTML doc, and that’s that. For any production app, though, you’ll probably want to selectively import only what you want, and there are instructions for that, too.
With Shoelace installed, let’s create a Svelte component to render some content, and then go through the steps to fully customize it. To pick something fairly non-trivial, I went with the tabs and a dialog (commonly referred to as a modal) components. Here’s some markup taken largely from the docs:
General
Custom
Advanced
Disabled
This is the general tab panel.
This is the custom tab panel.
This is the advanced tab panel.
This is a disabled tab panel.
Hello World!
This renders some nice, styled tabs. The underline on the active tab even animates nicely, and slides from one active tab to the next.
I won’t waste your time running through every inch of the APIs that are already well-documented on the Shoelace website. Instead, let’s look into how best to interact with, and fully customize these Web Components.
Interacting with the API: methods and events
Calling methods and subscribing to events on a Web Component might be slightly different than what you’re used to with your normal framework of choice, but it’s not too complicated. Let’s see how.
Semne de carte
The tabs component () are o
show
metodă, which manually shows a particular tab. In order to call this, we need to get access to the underlying DOM element of our tabs. In Svelte, that means using bind:this
. In React, it’d be a ref
. And so on. Since we’re using Svelte, let’s declare a variable for our tabs
instanță:
let tabs;
…and bind it:
Now we can add a button to call it:
It’s the same idea for events. There’s a sl-tab-show
eveniment that fires when a new tab is shown. We could use addEventListener
pe noastre tabs
variable, or we can use Svelte’s on:event-name
comenzi rapide.
console.log(e)}>
That works and logs the event objects as you show different tabs.
Typically we render tabs and let the user click between them, so this work isn’t usually even necessary, but it’s there if you need it. Now let’s get the dialog component interactive.
dialog
The dialog component () takes an
open
prop which controls whether the dialog is… open. Let’s declare it in our Svelte component:
let tabs;
let open = false;
De asemenea, are un sl-hide
event for when the dialog is hidden. Let’s pass our open
prop and bind to the hide
event so we can reset it when the user clicks outside of the dialog content to close it. And let’s add a click handler to that close button to set our open
prop to false
, which would also close the dialog.
open = false}>
Hello World!
Lastly, let’s wire up our open dialog button:
And that’s that. Interacting with a component library’s API is more or less straightforward. If that’s all this post did, it would be pretty boring.
But Shoelace — being built with Web Components — means that some things, particularly styles, will work a bit differently than we might be used to.
Customize all the styles!
As of this writing, Shoelace is still in beta and the creator is considering changing some default styles, possibly even removing some defaults altogether so they’ll no longer override your host application’s styles. The concepts we’ll cover are relevant either way, but don’t be surprised if some of the Shoelace specifics I mention are different when you go to use it.
As nice as Shoelace’s default styles are, we might have our own designs in our web app, and we’ll want our UX components to match. Let’s see how we’d go about that in a Web Components world.
We won’t try to actually îmbunătăţi anything. The Shoelace creator is a far better designer than I’ll ever be. Instead, we’ll just look at how to Schimbare things, so you can adapt to your own web apps.
A quick tour of Shadow DOMs
Take a peek at one of those tab headers in your DevTools; it should look something like this:
Our tab element has created a div
container with a .tab
și .tab--active
class, and a tabindex
, while also displaying the text we entered for that tab. But notice that it’s sitting inside of a shadow root. This allows Web Component authors to add their own markup to the Web Component while also providing a place for the content we provide. Notice the element? That basically means “put whatever content the user rendered între the Web Component tags aici. "
Asa ca component creates a shadow root, adds some content to it to render the nicely-styled tab header along with a placeholder (
) that renders our content inside.
Encapsulated styles
One of the classic, more frustrating problems in web development has always been styles cascadă to places where we don’t want them. You might worry that any style rules in our application which specify something like div.tab
would interfere with these tabs. It turns out this isn’t a problem; shadow roots encapsulate styles. Styles from outside the shadow root do not affect what’s inside the shadow root (with some exceptions which we’ll talk about), and vice versa.
The exceptions to this are inheritable styles. You, of course, don’t need to apply a font-family
style for every element in your web app. Instead, you can specify your font-family
once, on :root
or html
and have it inherit everywhere beneath it. This inheritance will, in fact, pierce the shadow root as well.
Proprietăți personalizate CSS (often called “css variables”) are a related exception. A shadow root can absolutely read a CSS property that is defined outside the shadow root; this will become relevant in a moment.
::part
selector
What about styles that nu inherit. What if we want to customize something like cursor
, which doesn’t inherit, on something inside of the shadow root. Are we out of luck? It turns out we’re not. Take another look at the tab element image above and its shadow root. Notice the part
atribut pe div
? That allows you to target and style that element from outside the shadow root using the ::part
selector. We’ll walk through an example is a bit.
Overriding Shoelace styles
Let’s see each of these approaches in action. As of now, mult of Shoelace styles, including fonts, receive default values from CSS custom properties. To align those fonts with your application’s styles, override the custom props in question. See documentele for info on which CSS variables Shoelace is using, or you can simply inspect the styles in any given element in DevTools.
Inheriting styles through the shadow root
Deschideți app.css
de fișier în src
directorul de StackBlitz project. În :root
section at the bottom, you should see a letter-spacing: normal;
declaration. Since the letter-spacing
property is inheritable, try setting a new value, like 2px
. On save, all content, including the tab headers defined in the shadow root, will adjust accordingly.
Overwriting Shoelace CSS variables
component reads an
--indicator-color
CSS custom property for the active tab’s underline. We can override this with some basic CSS:
sl-tab-group {
--indicator-color: green;
}
And just like that, we now have a green indicator!
Querying parts
In the version of Shoelace I’m using right now (2.0.0-beta.83), any non-disabled tab has a pointer
cursor. Let’s change that to a default cursor for the active (selected) tab. We already saw that the element adds a
part="base"
attribute on the container for the tab header. Also, the currently selected tab receives an active
attribute. Let’s use these facts to target the active tab, and change the cursor:
sl-tab[active]::part(base) {
cursor: default;
}
Și asta este!
Personalizarea animațiilor
For some icing on the metaphorical cake, let’s see how Shoelace allows us to customize animations. Shoelace uses the API-ul Web Animations, and exposes a setDefaultAnimation
API to control how different elements animate their various interactions. See the docs for specifics, but as an example, here’s how you might change Shoelace’s default dialog animation from expanding outward, and shrinking inward, to instead animate in from the top, and drop down while hiding.
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)" },
});
That code is in the App.svelte
file. Comment it out to see the original, default animation.
La finalul
Shoelace is an incredibly ambitious component library that’s built with Web Components. Since Web Components are framework-independent, they can be used in any project, with any framework. With new frameworks starting to come out with both amazing performance characteristics, and also ease of use, the ability to use quality user experience widgets which aren’t tied to any one framework has never been more compelling.