In the continuing saga of our Web Component building journey, we now turn our attention to allowing our web components to be styled. In case you missed it, we've been building a "loading modal" web component, and we've added some customizability via slots, properties and attributes already.
This component gives us a nice, simple way to drop a commonly-used feature into a web page. Load the component via <script>
tag, add a <eh-loading-modal>
tag somewhere on the page, and when needed, call the .show()
and .hide()
methods on the node.
What's still missing (and was promised in the previous article) is the ability to change the look of the component.
Let's add some style
Before we begin, let's review the inline-CSS of our component.
this.shadowRoot.innerHTML = `
<style>
#loading {
background-color: gray;
height: 100%;
left: 0;
opacity: 0;
position: fixed;
top: 0;
visibility: hidden;
width: 100%;
z-index: 1;
}
#loading.visible {
opacity: 97%;
visibility: visible;
transition: visibility 0s linear 0s, opacity .25s 0s;
}
#loading .message {
height: 100%;
margin: 20px 2px;
padding: 2px 16px;
position: relative;
text-align: center;
top: 40%;
vertical-align: middle;
}
</style>
...
There are several hard-wired assumptions here about how the modal will look. It starts right at the top, with background-color: gray;
and continues on with opacity: 97%
under the .visible
selector. These two lines give us the "mask" which ocludes the page when the component is visible. But what if we didn't want a 97% gray mask? What if we wanted sea-foam green?
The first thing you'd likely think of is something of this sort:
eh-loading-modal > div {
background-color: rgb(113 238 184);
}
Alas, this won't work. At least not as you expect, but more on that below. You may recall from the first article in this series that a component has a "mini-DOM" (shadowRoot) which sits inside the document's DOM. The issue here is that CSS selectors don't cross those boundries. Which makes sense, if you consider that our component is using IDs and class names for it's own purposes, and things could break badly if some arbitrary CSS from the page changed how our component worked, or we had multiple different components on the page, each with CSS which interferred with each other and/or the page itself. So 10 out of 10 to the web standards folks on that.
So how do we do it? More properties? Slots? Pan-fried salmon?
CSS custom properties to the rescue!
Nope, we'll leverage the fact that CSS custom properties DO cross those DOM borders!
Let's begin with our mask color:
this.shadowRoot.innerHTML = `
<style>
#loading {
background-color: var(--mask-background-color, gray);
...
}
#loading.visible {
opacity: var(--mask-opacity, 97%);
...
}
</style>
...
The var()
function is replaced with EITHER the value of the named custom property, if it's defined, or the value after the comma, if not. Because we've put our original values in as fallbacks to the custom properies, nothing will (yet) change in our POC page, which, you'll recall, is:
<html>
<head>
<script src="eh-loading-modal.js"></script>
</head>
<body>
<main>some content here</main>
<eh-loading-modal></eh-loading-modal>
<script>
const loader = document.getElementsByTagName('eh-loading-modal')[0]
loader.show()
setTimeout(() => loader.hide(), 5000)
</script>
</body>
</html>
Let's sea-foam that baby up by adding a <style>
block to the <head>
of our page
<html>
<head>
<script src="eh-loading-modal.js"></script>
<style>
eh-loading-modal {
--mask-background-color: rgb(113 238 184);
--mask-opacity: 88%;
}
</style>
</head>
...
</html>
Reload the POC and bask in the glorious customization!
Styling nodes in a slot
Above, I suggested that a selector like eh-loading-modal > div {
would work, but not as expected. If you are expecting that selector to pick up the first child div
INSIDE the component's shadowRoot, you will be disappointed; selectors don't cross that boundry. This is why we need to use CSS custom properties above.
However, if you put content between the start and end tags of the component (in one or more slot
s), THOSE nodes ARE selectable via CSS. To see this in action try the following in your POC page:
<html>
<head>
<style>
eh-loading-modal > div {
color: blue;
}
...
</style>
</head>
<body>
...
<eh-loading-modal>
<div>This will be styled</div>
</eh-loading-modal>
...
</html>
Styles applied to slots by the component's CSS will be overridden by the page's CSS, so adding custom properties to slotted elements isn't strictly neccessary.
Styles of style
There are several ways to apply CSS rules to components and slots within components, including in a separate css file, in a <style>
tag in either the <head>
or the <body>
or, interstingly, inside the component's tag.
Consider this:
<html>
<head>
<script src="eh-loading-modal.js"></script>
<style>
eh-loading-modal {
--mask-background-color: rgb(113 238 184);
--mask-opacity: 88%;
}
</style>
</head>
<body>
<main>some content here</main>
<eh-loading-modal>
<style>
* {
--content-text-align: right;
}
</style>
<div>This will be styled</div>
</eh-loading-modal>
<script>
const loader = document.getElementsByTagName('eh-loading-modal')[0]
loader.show()
setTimeout(() => loader.hide(), 5000)
</script>
</body>
</html>
Next steps
So now we have a component we can customize, interact with and style. Next time out, we'll discuss event handling, both internally to a component and between the component and the page.