Handling Styles In Web Components
January 2025
TL;DR
The number of examples in this post makes it kinda long. Here's the highlights:
-
You can create a script element in a web
component's
.js
file and append it to a parent page's stylesheets to avoid having to mess with the parent page's styles directly - You can use the attributes of custom elements to pass CSS variable names into a component that can then be used to call back to the parent page's CSS variables
The Component File section has a full code example if you'd rather just see code.
Introduction
One aspect of web components I struggle with is that they never feel self-contained. The main place I feel that friction is with styles. Specifically:
-
I'm having to apply a
display: block;
in the parent page's CSS to get my components to render properly. Or, I have to adddisplay: inline-block
if I want the behavior - Components have access to the CSS variables set on the parent page. That's great as far as it goes, but the way I'd been thinking about it, it required adjusting the CSS for the parent page itself to set specific variable names the component expects
In both cases, using the component requires messing with the parent page's CSS. I really don't like the way that feels. I had a couple realizations about how to address those issues. This page is for taking a look at them.
Fair warning: I'm still new to web components. I may be off-base here. Part of the reason I'm doing this write up it to solicit feedback.
With that said, here's what I'm doing:
Overview
There are two techniques I'm using to address working with styles:
-
Add styles to the parent page by creating a new element and
appending it to the page via
document.head
from the component's.js
file before defining the component itself - Pass the names of CSS variables (or specific values) I want to use into the component via attributes in the custom element's tag
I can use a component on a page without having to adjust the parent's CSS at all when I use these methods. The only thing I have to touch is the component's custom element tag and its attributes.
The Component File
Here's a full example where I'm using the techniques. This is also the code that powers the examples on this page:
const componentStyles = document.
componentStyles. = `
aws-wc {
display: inline-block;
}
`
document..
customElements.
We'll dig into the details in a moment. Before we do, here's a look at how it's used:
Basic Usage
The component's filename is component.js
.
It's included via this tag that's
placed just before the page's closing body
tag:
Example 1
The component provides a custom element named aws-wc
(where "aws" stands for alan w smith. Not
the web services company) that outputs the word "Ping"
with a background color behind it. The basic use looks like this:
Code
Result
The component defaults the background color
to blue
. The text color is
inherited.
Example 2
The component is designed to do very little. The idea being to keep the examples as clear as possible. One feature it does have is the ability to pass a color to use for the background via a "backgroundColor" attribute like this:
Code
Result
Using CSS Variables
Even better than being able to pass explicit colors is the ability to pass CSS variables defined by the parent page's styles.
Example 3
This page includes the following in its stylesheets:
}
--accent-color-1
is what's used for the
borders of the code/result example blocks
on this page. We can set a component to use the
same variable for its background like this:
Code
Result
The name of the variable is passed into a function that builds the stylesheet for the component's shadowroot. Once there, it grabs the associated value from the parent page's styles like a regular CSS variable call.
Example 4
Component instances are isolated from each other. We can use different variables for each one:
Code
Result
Example 5
The same technique can be used with multiple
styles. Here's an instance that
uses a textColor
in addition to the
backgroundColor
:
Code
Result
Adding Styles To The Parent Page
It's time to start looking at the component file itself.
Our aws-wc
component is designed to work as
a display: inline-block element.
We have to set the style to avoid it taking up an
entire row.
I originally thought this
meant having to modify the parent page's CSS directly.
That's not the case. We can do it in the same .js
file that contains our component by adding a
stylesheet outside the definition of the component's class.
Here's the top of the component.js file where we do just that:
const componentStyles = document.
componentStyles. = `
aws-wc {
display: inline-block;
}
`
document..
This fires when the script is loaded on the page. It only fires once instead of each time an instance is created because it's outside the component's class definition.
Passing Style Variables To The Component
Using The ShadowDOM
The component is set up to use the shadowDOM with the
this.attachShadow({mode: 'open'})
call
in the constructor:
Getting The Attribute Values
Pulling style variables (or, explicit values) in from
a custom tag's attributes takes a few steps. The first
thing is to add a function to the component's
connectedCallback()
to grab the values
from the attributes. Here's the code where I'm calling
a this.getColors()
function:
The function looks like this:
The code checks to see if the color attributes exist in the custom element. They get used if they do. Otherwise, a default fallback gets set.
Applying The Styles
The internal stylesheet for the component is
generated by the styles()
function:
It uses the this.backgroundColor
and
this.textColor
variables defined by
the getColors()
function. They are
written in as strings which is what allows them
to accept CSS variable names.
There's no difference to the component compared to hard coding the variable names.
The styles are added to the shadowroot from the
connectedCallback()
function with:
this..
Adding The Content
The body of the component is defined in the
template()
function:
The connectedCallback()
function adds the
template to the shadowRoot
with:
const contents =
this...
this..
Finishing The Component
The last step is to add the component's definition to the page with:
customElements.
Conclusion
Using these techniques provides a way to make components that don't require messing with a parent page's stylesheet. It's a much nicer separation of concerns. It's made me genuinely more excited about making components.
Endnotes
- This was my first run at this idea. I've received feedback from folks who know more about this stuff than me that this works fine but there are some potential improvements. See the TODO section below for those notes. (I'll update the actual examples when I'm able)
- I've tested this on Chrome, Firefox, and Safari on my mac, and Chrome, Edge, and Firefox on my windows machine. Everything is working the same across the board.
-
As mentioned in the Introduction, I'm still new to working with web components. It's possible all this stuff is well known in general or that there are other, better ways to do the same things.
Part of the reason for this post it to solicit feedback from folks with more experience. If you see something amiss please let me know.
TODO
-
Switch to a
getAttributes()
function to pull in the attributes in a single loop instead of having to do them explicitly by name. Then, you can set defaults on just the colors in their own function. This puts things more in line with my general approach to components. -
Look into
CSSStyleSheet
as another approach for making the stylesheet. For example:customElements.define('aws-wc', class extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); const styles = new CSSStyleSheet(); styles.replaceSync(`:host { display: block }`); this.shadowRoot.adoptedStyleSheets.push(styles); } });
- Another possible approach for adding the stylesheet to the parent document is to do it in the connectedCallback. The idea being to use an id that's added to the style tag. Before the component attempts to create it, it would check to see if it already exists.
-
Another suggestion for the id on the style tag is that
it could be used as a more general place for components to add
styles without having to make new
<style>
tags all the time - Check out A mental model for styling the Shadow DOM for some more thinking about the way styles interact
- Note that the attributes are not responsive. See Responding to attribute changes in Using custom elements if that's a concern