iTranslated by AI
The Next Era of CSS-in-JS Will Embrace Web Components
CSS in JS is a programming methodology for writing CSS within JavaScript code, with styled-components being one of the major players. In modern development, we often rely on CSS in JS when using frontend view libraries like React or Vue.
In this article, I will talk about the idea that the next generation of CSS in JS might be based on Web Components.
Reviewing Web Components
Web Components is a collective term for several Web standards, with Custom Elements and Shadow DOM being particularly important. Both versions of the specifications known as V1 are supported by all modern browsers (though I'm slightly concerned that Safari is a step behind).
Custom Elements is a feature that allows you to register unique element names like <x-foo>...</x-foo> and define processes for when those elements are created. Shadow DOM is even more important, so I will explain it in a bit more detail for those who aren't familiar with it yet.
Shadow DOM
Shadow DOM is a feature that allows you to define something like the "internal implementation" of an element. For example, the following div element has a simple structure with just a p tag inside.
<div id="area" style="border: 1px solid #666666">
<p>Hello, world!</p>
</div>
However, by attaching a Shadow DOM to the div element, you can give the div element a complex structure.
const div = document.getElementById('area');
// Attach a Shadow Root to the div
const shadowRoot = div.attachShadow({ mode: 'open' });
// Define the content of the Shadow Root
shadowRoot.innerHTML = `
<style>
p {
border: 1px solid red;
}
</style>
<p>↓↓↓↓↓</p>
<slot></slot>
<p>↑↑↑↑↑</p>
`;
When you do this, the div element will be displayed as follows:
In other words, by attaching the Shadow DOM to the div element, it is rendered as the content of that div element. Also, you'll notice <slot></slot> within the Shadow DOM; this is where the children of the div element (in this case, <p>Hello, world!</p>) are placed. Consequently, the div element is displayed as if it has the following content:
<div id="area" style="border: 1px solid #666666">
<style>
p {
border: 1px solid red;
}
</style>
<p>↓↓↓↓↓</p>
<p>Hello, world!</p>
<p>↑↑↑↑↑</p>
</div>
However, it is worth noting here that a style element is written inside the Shadow DOM. This contains style specifications for p, but styles written within a Shadow DOM only affect elements within that same Shadow DOM. Therefore, border: 1px solid red; is applied to <p>↓↓↓↓↓</p> and <p>↑↑↑↑↑</p> which originate from the same Shadow DOM, but it is not applied to <p>Hello, world!</p> because it does not originate from the Shadow DOM.
Conversely, even if you write a style like the following outside of the Shadow DOM, it will not affect the inside of the Shadow DOM.
/* Written outside of the Shadow DOM */
p {
color: blue;
font-size: 2em;
}
In this way, styles are isolated between the inside and outside of the Shadow DOM. This is the most important factor when considering CSS in JS.
View Libraries and Web Components
As the name "Components" suggests, Web Components is an attempt to bring the concept of components to Web standards. On the other hand, we are familiar with the concept of components through view libraries such as React and Vue. Therefore, Web Components is often seen from the perspective of "being able to create components without using libraries" or "migrating from view libraries to Web standards."
However, my view is different. View libraries are libraries and ecosystems built on top of the Web standard called DOM in the first place. If so, the evolution of Web standards will not lead to the decline of view libraries, but rather to their evolution. It is expected that the ecosystem of view libraries like React will also transition to something built on the foundation of Web Components. This is the prediction of this article.
And I believe that CSS in JS is relatively well-positioned to benefit from Web Components. This is because Shadow DOM provides exactly the functionality needed for it, while existing CSS in JS still lacks an absolute, singular solution and is considered to have room for evolution.
The ecosystem related to CSS in JS will evolve into a new form by benefiting from Web Components. This is what it means to subjugate Web Components.
Many of you may have thoughts like, "Even so, I've never used Web Components or Shadow DOM, and the learning cost... and this flaw and that flaw..." That is not a problem. This is because this article is talking about the next era, not telling you to adopt Web Components right now.
In fact, Web Components are still in the middle of evolution. I am also hopeful about Declarative Shadow DOM, which has been a hot topic recently. With this, we can handle SSR nicely with Web Components-based CSS in JS (conversely, without this, SSR for Web Components is quite tough).
Current CSS in JS
Before we talk about CSS in JS in the Web Components era, let's take a quick look at current CSS in JS.
The troubling part of doing CSS in JS is how to localize styles. Regarding current best practices, my opinion is close to the following articles (assuming React + styled-components).
- How to write ReactComponents that withstand aging
- Refactoring style isolation under a CSS in JS environment on a blog
I will quote examples of DOM layer and Style layer components from the first article.
// DOM layer
const Component: React.FC<Props> = props => (
<div className={props.className}>
<button onClick={props.handleClick}>
{props.flag ? 'click me' : 'CLICK ME'}
</button>
</div>
)
// Style layer
const StyledComponent = styled(Component)`
> button {...}
`
In other words, it is a method where you prepare a component responsible only for the DOM structure and then apply styles to that DOM structure in the Style layer. The advantage of this method is that you can write styles collectively for the DOM structure. That is, in the example above, the styles for both the div and the button defined by Component are consolidated into StyledComponents. In CSS, design is often realized through the coordination of parent and child styles, as represented by display: flex. The natural form corresponding to this is likely writing styles collectively for a DOM structure of a certain size, rather than for individual elements.
By the way, I prefer this way (since passing data based on className is bothersome). Though this itself has the disadvantage that the tag name ends up in a separate location.
// DOM layer
const Component: React.FC<Props> = props => (
<Wrapper>
<button onClick={props.handleClick}>
{props.flag ? 'click me' : 'CLICK ME'}
</button>
</Wrapper>
)
// Style layer
const Wrapper = styled.div`
& > button {...}
`
However, in my view, these have not yet reached their final form. There are two main reasons for this.
First, the "DOM structure" and the "styles for it," which are originally one entity, must be described in two separate layers. Second, we must be careful about style leakage. Regarding style leakage, the previous article contains the following description:
Don't forget to defend against specification leakage to children using
>.
In other words, if you accidentally write & button instead of & > button in the previous example, the styles will apply to buttons that the Wrapper is not concerned with (parts provided as child elements from the outside), so you have to be careful not to do that.
These two problems will be solved in the Web Components era.
CSS in JS in the Web Components Era
Using Shadow DOM, the previous component would roughly look like this. First, put the following HTML inside the Shadow DOM of the Component component:
<style>
button {
...
}
</style>
<div>
<button>
<slot></slot>
</button>
</div>
The user would use it like this. The logic position is slightly different from the original component, but this form is used because it's better to completely separate logic from style (which is more suitable for the Shadow DOM-based approach).
<Component>{
flag ? 'click me' : 'CLICK ME'
}</Component>
By doing this, the "HTML structure" and the "styling for it" can be lumped together into the Shadow DOM, resolving the issue of having to write them separately. Furthermore, regarding style leakage, the Shadow DOM is rock-solid. It is the most robust despite being the most concise description. In fact, if you have more complex styles, you can even use classes freely.
<style>
.submitButton {
...
}
</style>
<div>
<button class="submitButton">
<slot></slot>
</button>
</div>
You don't have to worry about class names overlapping with others. This is because even if there is a style like .submitButton { ... } outside the Shadow DOM, it cannot affect the inside of the Shadow DOM[1]. What an ideal world.
However, there are still points I haven't fully considered. The onClick handler from the original component has disappeared from the component above. I haven't quite found the answer yet for what is the most natural way to handle events in the context of CSS in JS. I'm in the middle of thinking about it. Well, it's a story about the future, so I'll just think it through by the time that future arrives.
react-wc
Actually, based on the ideas above, I've already created a library called react-wc.
It is already usable for certain purposes, but it's more aimed at being a next-generation CSS in JS library that truly carries the "future." For details, please check the corresponding blog post, but for CSS in JS purposes, it can be used as follows. The specification is very simple: just specify what you want to put into the Shadow DOM as a string. The content inside the Shadow DOM is not expected to be updated; such things should be passed using slot() (equivalent to <slot></slot>).
import { html, slot } from "react-wc";
// A React component is created
const Component = html`
<style>
.submitButton {
...
}
</style>
<div>
<button class="submitButton">
${slot()}
</button>
</div>
`;
// Can be used as a React component
<Component>{flag ? "click me" : "CLICK ME"}</Component>
Summary
In this article, I predicted the future of CSS in JS based on Web Components. Since various issues with current CSS in JS can be solved by Shadow DOM, my prediction is that once the preparations are complete, things will move in that direction. Also, I expect that Web Components will not make React or Vue obsolete, but rather (at least for a while), view libraries will evolve by utilizing Web Components.
Many people might feel that Web Components and Shadow DOM are still novelties, but since they are part of Web standards, they occupy the same position as "vanilla DOM," so to speak. If we keep avoiding Web Components because "it's still early...," the future that awaits is one where "people can use React/Vue but cannot use vanilla DOM" (opinions will differ on whether that's a good or bad thing, but I think it's bad).
Isn't it about time we start thinking about the next era with Web Components in sight? I've started thinking about it, so I created react-wc, which I introduced earlier. React-wc will continue to evolve along with the evolution of Web Components. If you start praising or contributing to react-wc now, you might become a pioneer of the next era.
-
Actually, you can use
::partsto apply styles from outside the Shadow DOM to the inside. However, this is opt-in. In other words, you need to declare from inside the Shadow DOM that "external styling is allowed for this element." ↩︎
Discussion