The easiest way to create web components with a distinctive visual style is to bake that style directly into the components' code. Most user interface components are designed to be used solely within the company creating them, in which case baking in styles may be acceptable. But anyone aspiring to create or consume reusable general-purpose web components will have to grapple with the fact that styling components is currently an unsolved problem.
That's worrisome. Most web components we've seen have a built-in visual style so distinctive that, without modification, it would look out of place in an app with someone else's brand. Not being able to easily theme such a component limits its utility.
Suppose you're writing a hypothetical custom element called reusable-component
and want to let other people style that element. Let's consider your options for doing so.
If you think someone wants to change the background color of your reusable-component
, you can define its background-color
style with a CSS Custom Property:
:host {
background-color: var(--reusable-component-background-color);
}
and your users can then style your component by defining that custom property:
:root {
--reusable-component-background-color: blue;
}
With the recent release of Edge 16, the above would now work in all modern browsers.
Unfortunately, this just doesn't scale well. It's a pain for you, who must define a new custom property for every CSS attribute someone might conceivably want to override. If your component has internal shadow parts — buttons, etc. — that your users might want to style, you have to define new custom properties for all the interesting CSS attibutes on all those parts. And this will be painful for your users, who have to learn a long new list of custom properties for every component they might want to style.
To solve the above problem, there's a proposal for CSS Shadow Parts which would make it possible to expose designated internal parts as pseudo-elements that can be styled from the outside. This is similar to the way you can style certain native HTML elements in a non-standard way with certain pseudo-elements. For example, WebKit exposes the thumb (handle) of a scroll bar as a pseudo-element ::-webkit-scrollbar-thumb
, so you can write
::-webkit-scrollbar-thumb {
background: pink;
}
to make scroll bar thumbs pink.
The CSS Shadow Parts spec would let you expose your component's internal parts for styling. If you had a commit button in the shadow of your reusable-component
, you could expose it as a part:
<button part="commit-button">...</button>
And someone can then write:
reusable-component::part(commit-button) {
background: pink;
border: 1px solid red;
border-radius: 5px;
}
to style the button. That's a more convenient way to achieve the same thing as CSS Custom Properties. There's less to document for your users and more flexibility. Your users can apply whatever styling they want without you needing to anticipate everything they might want to do.
There are some downsides, though, when it comes to reusing such a styled custom element.
One key advantage to giving your customer a custom element is that they end up with a single thing that produces a consistent result. But if your customer tries to style your reusable-component
, they'll end up creating a new reuse problem for themselves. People at that company now have to deal with two separate things: 1) your original reusable-component
definition, and 2) a stylesheeet with the company's styling for reusable-component
and its parts. On their own, those two entities are independent, with no explicit relationship. Having to track and apply them correctly creates maintenance headaches.
Your customer could define a new component-wrapper
element that wraps your original reusable-component
and applies the desired styling:
<template>
<style>
reusable-component::part(commit-button) {
background: pink;
border: 1px solid red;
border-radius: 5px;
}
</style>
<reusable-component>
<slot></slot>
</reusable-component>
</template>
Then your customer can distribute this component-wrapper
component internally, and everyone gets both your internal reusable-component
and the correct styling in a nice package. (Even if reusable-component
is actually defined elsewhere, component-wrapper
can express that dependency, so the pieces are linked together.)
But this introduces new challenges:
reusable-component
may have styling that's contingent upon CSS classes or attributes applied to the host element. Unfortunately, that host element is now sitting inside the shadow of the customer's component-wrapper
. Again, the customer writing component-wrapper
may have to carefully reflect any classes or attributes to the inner reusable-component
. Even if they do that correctly, that may still end up with unexpected behavior.reusable-component
needs to be styled to fill the host component-wrapper
, so that if the latter is stretched, the former will be stretched to fit. That's not hard, but could easily be forgotten.padding
to the component-wrapper
, that will apply to the wrapper, not within the inner reusable-component
as they may intend. The customer could expose the inner reusable-component
as a new part
to address that, but that introduces complexity and conceptual overhead.querySelector
that looks for reusable-component
won't match component-wrapper
. And an instanceof ReusableComponent
check will fail when applied to an instance of WrappedComponent
. The customer could potentially implement Symbol.hasInstance
on their class, but that's getting complex. In general, it'll be real work for your customer to create component-wrapper
as a drop-in replacement for your reusable-component
.role="none"
to the wrapper to keep it out of the accessibility tree. But if component-wrapper
is given a tabindex
, a screen reader might get confused when keyboard focus moves to the component.Overall, without an easy repackaging mechanism for themed components, organizations may have difficulty adopting and styling components they acquire from elsewhere.
It's been our experience that even general-purpose components can end up with complex styling. People tend to approach component styling/theming as if the components were completely static, but components have dynamic state. State is often implicated in styling. As an example, a native button that's disabled
shows different styling than an enabled button. If someone isn't carefully considering the :disabled
pseudo-class in their button styling, they may end up applying an enabled button appearance to a disabled button.
Web components can have complex internal states, resulting in correspondingly complex internal stylesheets. Overriding such styles will be a delicate matter. To look at some concrete examples, I looked through some internal stylesheets in web components we've previously written. Here are some of the CSS selectors I found:
// From a carousel component
:host(.overlayArrows) .navigationButton:hover:not(:disabled) { ... }
// From a tabs component
:host([generic=""][tab-position="right"]) .tab:not(:last-child) { ... }
// From a toast component
:host([from-edge="bottom-right"].opened:not(.effect)) #overlayContent,
:host([from-edge="bottom-right"].effect.opening) #overlayContent { ... }
Each complex CSS selector in your component may create a challenge for your customer. If the tabs component above exposes an individual tab
as a part
, what about that :not(:last-child)
bit? If your customer writes styles that target the tab part, what styling should apply to the last tab?
In general, even if you can expose an interesting internal part of the component for outside styling, your customer will need to be aware of a large number of conditions that may apply. They could easily end up writing rules that don't apply (because more specific conditions exist that take precedence) or apply when they shouldn't (they write rules that are too general).
This is not to say that the CSS Shadow Parts spec won't be a step forward — it will be — but rather to say that styling components with of normal complexity might turn out to be extremely challenging in practice.
(Aside: It goes without saying that, even if the browser vendors are excited about CSS Shadow Parts and the spec speeds through the standards process, it could still be a very long time until you can take advantage of them in all the browsers and devices you care about. And for what it's worth, polyfilling new CSS syntax is notoriously difficult to do well.)
Your customer trying to use your reusable-component
might accomplish a certain degree of styling by applying inline styles to the component's host element (the top-level <reusable-component>
instance sitting in the light DOM). Users of React and other FRP frameworks have found inline styling a powerful way to have a component apply styles to subelements. And more generally, inline styling is usually the easiest way to programmatically adjust an element's appearance regardless of framework.
However, there are serious challenges using inline styles with web components. Inline styles can't be used to style internal component parts. That will remain true even if and when the CSS Shadow Parts proposal is adopted, as that only addresses styling with stylesheets. And as noted above, components can have complex state. Writing inline styles for the host element that apply in all conditions is likely too blunt an instrument.
React and similar frameworks already struggle somewhat to deal with styling, but an increase in the presence of complex general-purpose components will make the issue more pressing.
Given the above, we're not sure that either CSS Custom Properties, CSS Shadow Parts, or inline styling will be sufficent. We think those platform features are really interesting — it's just that they may not be enough for what we want to do. We want to create reusable web components that companies can easily brand for their applications, and we're unsure how to deliver that.
We're exploring alternative ideas for letting people style our web components, but are very interested in hearing how other people are tackling this problem. If you have ideas, please share.