Creating a Public Styling API
Hi there and welcome to another exciting Build it with Lit video. I'm Andrew, a software engineer working on Lit at Google. Today I will show you two powerful tools that let users of your element style it.
Our starting point is the simple Carousel we built in the last episode. We also easily used it in React. If you haven't watched that video, it's okay because what we learned today applies to all Lit elements and all web components.
If you don't know what Lit is, check out the very first video in the Build it with Lit playlist or check out the tutorials over on our lit.dev website. TLDR: it's a tiny, fast library for building web components that work everywhere, with any framework or none at all. In this video, we will apply two tools to make it so our simple Carousel element can be customized.
First, I asked a designer to create three super different carousels. If our Carousel can be customized to look like any of these, it can definitely be made to look like anything.
In technical terms, we will define a public styling interface for our simple Carousel element using CSS custom properties and CSS Shadow Parts. If this meant nothing to you, then you're watching the right video. By the end, you'll have the tools needed to declare an explicit styling interface for your elements. And if you're using web components, you'll know how to look for and use those tools to customize their styling.
Understanding Shadow DOM Style Encapsulation
Before we jump into the carousel, I want to give you some background on the default behavior of styling when using Lit elements. This addresses a question that came in asking, "Won't the simple selectors we wrote in the last video leak out and impact the page?" And the answer is no.
Styles used in a Lit element are scoped by default, and this Shadow DOM prevents styles from leaking out of the component and also stops most styles from getting in. Let me show you. Link is in the description if you want to check it out yourself. On the left is our HTML and on the right is our preview. In the preview, note the top paragraph has an orange background, the middle is unstyled, and the bottom one has a blue border.
Our HTML has a style tag that selects all paragraph tags and draws a blue border around them. Then we have the three tags: two Lit elements, styled-p and unstyled-p, and a paragraph element.
If we take a look at our two Lit element definitions, they both render a paragraph tag. The styled-p element styles paragraph tags with an orange background color. The unstyled-p element has no styles. Let's look at that preview again. Notice that all the styles are scoped by the browser by default. None of the styles are interfering with one another, and that's beautiful. When you have lots of components with lots of styles, they won't break each other.
Let's look at this example as a donut diagram. We can visualize our Lit elements as donuts. The host is the Lit element. The blue boundary surrounds the shadow Dom, which contains the component's styles and template, and any children projected into the shadow Dom by a slot element end up in the donut hole, for example, the slides of the carousel.
Let's make this donut concrete. We can swap out the host with styled-p. Within the shadow Dom, we have the element's template and styles. Now I've added the global styles. By default, the styles do not cross that blue line. The background color doesn't leak out of the donut and the border doesn't leak in. And that's why the unstyled-p element isn't styled by the global styles; its paragraph template is within the blue line boundary created by the shadow Dom.
If you're using Lit elements, you can have confidence that they won't leak styles. And if you're an element author, your elements will look how you want them to everywhere. Lit elements shift styles to something where you, the author, are in control. But what if you want to let users customize your element styling? Then you must define an explicit styling interface to provide a way to cross that blue line in the diagram. And that is where the two tools come in.
Styling with CSS Custom Properties
Starting with the simplest design, we need to allow our simple Carousel customers to customize the carousel so it looks like this. And when they press the button it should become orange. Notice that all of these customizations can be made by changing simple CSS properties: text color, background color, and box shadow.
But how do we let users apply these changes to elements within our carousel's shadow root? Well in the case of text color, it turns out to be easy because certain properties like colors and font settings automatically inherit through the shadow root boundary.
If you use CSS, this probably doesn't surprise you. After all, when you set the text color on any element, you expect all of its descendants to take that same color. Imagine the pain if you needed to set the color of every node individually. And you also probably have the intuition that other properties like background color and box shadow don't inherit.
To achieve our design, we need to find a way for elements in the carousel's shadow root to selectively inherit property values that the user provides. That's where our first tool, custom properties, comes in. Let's see how it works.
For each property that we want to make customizable, we can define a custom property. The names of these custom properties are part of your public styling API.
These are all the styles in the simple Carousel we wrote last time, and we specifically want to make this box shadow public so it can be customized. Instead of this constant box shadow value, we can wrap it with a var() function, which will use the custom property --carousel-box-shadow if it is defined, and fall back on the default styles if the variable is not defined. This lets us keep our default styles when no customization is provided.
I'll do the same for the slide button element. The box shadow when the button is not in a pressed state needs to be a custom property, and the box shadow when the button is pressed down also needs to be a custom property. The button at rest can use the same custom property: --carousel-box-shadow.
And for the active button, let's make a new custom property: --carousel-active-button-box-shadow.
We're not done with the pressed button. We want to create a custom property to customize the background color and the SVG color. Now we've set up all the custom properties we need. But there's one more little detail to take care of. Remember how we said text color automatically inherits? Well it does, but what if you want to apply the current text color to an element that's not actually text, like the SVG chevrons in our carousel buttons? The color of the chevrons is currently hard-coded, but by setting the chevron stroke and fill properties to the currentColor value, we can make them match the inherited text color. Neat!
With these changes, we have made it possible to customize the simple carousel.
Demo: Using Custom Properties
Did you feel that? Someone has just npm installed the simple carousel.
Whoa, cool carousel library I just found on the interwebs. It's super easy to install with just a script tag. I have some amazing content to put in it. Not quite the look I'm going for, though. I want to change the typography, which I know inherits. It's subtle, but the text updated. And now we can use the custom properties to customize the rest.
The box shadow should be a subtle elevation, and the button shadow should match. The button font color should be white when active, with an orange background. This is exactly what I wanted, and I did it with only custom properties. And that's design number one.
When to Use CSS Shadow Parts
Ah yes, we just need to write the "make the carousel totally different and square" custom property. Our custom property hammer is not enough to hammer this carousel into this shape. Also, that second design needs to be customized to remove the buttons. Custom properties are fantastic at making individual CSS properties public, but sometimes we want to give the user access to DOM nodes so they can apply any styles they want. This is when tool two, the part attribute, is useful.
Before I go over another new concept with you, punch in, please. I want to answer the question, "What makes Lit different to many other front-end options?" When Lit can do something the browser way, it always does it the browser way. That means when you want to do your own research and look up custom properties, you can look at lit.dev's styling page, but you can also read about it on MDN. Same with the next concept on the part attribute and selecting that part. This is why Lit is so interoperable. It uses the browser's component model, meaning your elements will work anywhere HTML works. Back to parts.
Implementing Shadow Parts
This is how we wrote the simple Carousel template in the last video, with the left and right button and main container. But now if we add part="container", someone using our carousel can customize this part by using the ::part() pseudo-selector and passing the part name. Selecting the container looks like this.
Now, looking at the carousel buttons, if we give them both the same part name, buttons, then the buttons part refers to both of them. The part attribute accepts a space-separated list of names, so now the left button and right button can be customized individually.
The slide button is also a custom element we wrote last time. How can we expose parts from inside it? Within the slide button element we can expose this div to the simple Carousel by adding the part name internal-button. This makes the part public to the direct parent, simple Carousel, but not outside the simple Carousel element.
In the simple Carousel, we can use another attribute, exportparts, to forward the parts from the slide button. This exportparts attribute exports the internal-button part from slide-button under the new name buttons. Now when someone selects the part buttons, they'll get all these elements together.
And this completes tool two, Shadow Parts, and completes the public styling interface for the simple Carousel element. Users of the simple Carousel can now style the carousel container, the buttons, and the element in the buttons.
Demo: Styling with Shadow Parts
Someone else is downloading the carousel from npm. Let's check out how they customize it. I bet you they'll use the parts we just exposed.
Whoa, cool carousel library I just found on the interwebs. It's super easy to install with just a script tag. I have some amazing content to put in it. Not quite the look I'm going for though. I can select the container part and make it a gray rectangle, overriding all the default styles the container came with. I can select the buttons, make them square, as tall as the carousel, and blue squares. The drop shadow is a filter applied to the simple carousel element. This is almost done. Just need to style the button's active state so they turn orange when pressed. We can select all the nodes with the buttons part and the :active pseudo-class to apply the orange styles when the button is pressed. And that's done.
We used the public styling interface of this simple Carousel element to style it. That was so fun, let's do it again.
Whoa, cool carousel library I just found on the interwebs. It's super easy to install with just a script tag. I have some amazing content to put in it. Not quite the look I'm going for though. This time I'll make the font white, the container part square and orange, and remove that box shadow. We can hide the buttons with, you guessed it, by using display: none on the buttons part. What's a carousel without a nice dark orange drop shadow? Since this carousel has no buttons, we're going to use a timer to increment the carousel slide every three seconds.
An automatic carousel, your users will totally love you for creating something like this. Please don't.
Examples, Resources, and What's Next
And that's it. Let's close this video with a stellar example of a web component that has great documentation about its public styling interface: our very own playground element. Here it is on GitHub, and here is the exact same element used in the lit.dev playground. Looks pretty different.
On GitHub, there's a section showing all the custom properties and their default values, and the shadow parts. There's even this configurator page which lets you change the styles through an interface and gives you the code to copy-paste so you can keep that customization. Awesome.
We've come a long way. If you want to take your time and read over the source code in this video, it's uploaded to our video series samples GitHub repo with the three carousel demos. Also, since the last time you and I had this one-on-one time, the Lit team released an awesome carousel tutorial on lit.dev. Check out Build an Animated Carousel tutorial over on lit.dev. It's a superb way to build a carousel written by the Lit team. Not only is it super nice, it also uses the Lit Labs motion package to create the beautiful animation in a really elegant way.
Next time we're going to dive into reactive controllers to share some logic in a nice decoupled way. Now that's recorded into this video, it has to happen.
Links to all the socials down below: Twitter, Slacks, GitHub, lit.dev. And if you build anything in Lit, let me know. This is called Build it with Lit, so I naturally get very excited and fascinated when someone shares a thing built with Lit. What else? Comment, subscribe... I will never build another carousel.