As web developers, we create websites. As designers, we want to make components. Since the platform was not originally designed for this, there can be papercuts.
Ideally, we would be able to create isolated components, and have the styles for them not leak out into any other parts of our application.
I'm going to go through as many ways I can think of at doing this, to demonstrate how much of a problem this is. I'll be making a "Card" component. A card really is just something visually distinct from the other parts of your website, as far as I can tell.
I'll go over:
- Vanilla CSS
- Component Libraries
- Web Components
Vanilla CSS
Stuff you can do that isnt like, a component library like React or Svelte.
Don't scope at all
Make a plain css file!
body > : is ( header , footer ) {
background-color : LightGrey;
}
. card {
background-color : LightSlateGrey;
}
Issues:
- We have everything in one CSS file, this can get a bit unwieldy...
- We might want to structure our CSS to prevent this
- We might introduce a global rule (e.g. all
h2s are green) that might not want to be in certain areas of the website- This is why we might want to scope our rules
| Requirement | Met? |
|---|---|
| Good Structure | Not really |
| Scoping | No |
BEM
BEM is a naming convention for components that tries to solve the scoping issue.
In BEM, you name your classes like this: .Block__element--modifier. (There seems to be debate on whether to use __ or - to designate elements. GetBEM and Bem.info disagree on this, and there appears to be no authoritative sauce …)
< div class ="Card ">
< h2 class ="Card__header "> This is the card's Header</ h2 >
</ div >
. Card {
background : LightSlateGrey ;
& __header {
color : Green ;
}
}
This is nice, and the syntax is quite easy to work with, so it feels like it would be a good solution.
Sadly, you do need a CSS preprocessor, Sass, for the above functionality.
In vanilla CSS, you'd have to spell out .Card__header and et cetera. Annoying on a large codebase, I would assume. (I suspect that standards/vendor people liked Sass's & syntax, but "string concatenation on arbitrary selectors" was not a very attractive thing to add to CSS, so they compromised on a still-pretty-good solution.)
The other issue is that this requires some level of discipline to keep up. It's a constant effort to make sure that you make everything a class, name it according to BEM, just to solve scoping issues.
| Requirement | Met? |
|---|---|
| Good Structure | Quite well (I would put each "block" in a separate file) |
| Scoping | Good, if you can keep it up |
Nesting
. card {
h2 {
color : Green;
}
background : LightSlateGrey;
}
Nesting is cool! It means you can scope your CSS to the parent container without chaining selectors like .card h2. You can also set a lower bound, which is something that BEM can do
| Requirement | Met? |
|---|---|
| Good Structure | Maybe |
| Scoping | It's okay, specificity can get high if you nest a lot. |
@Scope
@scope (. card ) /* to (.lowerBound, .otherBound) */ {
: scope { /* targets .card */
background : LightSlateGrey;
}
h2 {
color : Green;
}
}
Very similar to nesting. Unlike nesting though,
it handles specificity very differently.
While you can think of nesting as "resolving" to a non-nested CSS,
@scope can't be resolved in this way.
Scope does priority in rule matching very differently, based on proximity rules. MDN has a good explainer on this. I'd love to go more in depth on priority in a later blog post, as it seems like a quite advanced topic in CSS.
| Requirement | Met? |
|---|---|
| Good Structure | It's better than nesting |
| Scoping | Very well |
In Component Libraries
Focusing on React (but probably applicable elsewhere):
CSS Modules
. card {
background : LightSlateGrey;
h2 {
color : Green;
}
}
import styles from "./Card.module.css"
export default function Card {
return (
< div className = { styles . card } >
< h2 > This is the card's Header</h2 >
</div >
)
}
CSS Modules only scope classes. In fact, they rewrite your classes for you. (Note that you can sidestep this magic through :global(.selector) if needed.) This would be a "foot gun":
. card { /* ... */ }
h2 {
color : Green;
}
So you're not really writing scoped CSS. You're writing normal CSS where the classes happen to be scoped. At least it means you can reuse class names, I guess!
| Requirement | Met? |
|---|---|
| Good Structure | Decent, though you do need to make sure to make sure everything is focused on classes… |
| Scoping | Autogenerating unique class names works well for scoping |
CSS-in-JS
I don't really have enough experience with these libraries to touch on them, but CSS-Tricks has a good analysis.
They all have some funky way of inputting CSS though, like a Lit-style tagged template or an object.
Tailwind
< div class ="bg-[LightSlateGrey] ">
< h2 class ="text-[Green] "> This is the card's Header</ h2 >
</ div >
We are no longer writing CSS.
I'm lumping in Tailwind with React because it really shines when you are using a real component library.
Since you're already naming your components in the class and file name it does feel quite redundant to repeat naming things.
It also feels horribly lazy, like React…
Not writing CSS really does help with scoping though. Since you're declaring every element with classes that point to it directly, you aren't going to have any issues with it targeting other elements. This is a core design idea of Tailwind (and where I philosophically oppose it!)
| Requirement | Met? |
|---|---|
| Good Structure | Worse than any CSS |
| Scoping | Very |
Style property
It's a property, not an attribute.
< div style = {{ background : "LightSlateGrey" }} >
< h2 style = {{ color : "Green" }} > This is the card's Header</h2 >
</div >
weh.
| Requirement | Met? |
|---|---|
| Good Structure | :( |
| Scoping | Very |
Web components
Finally. I've talked about this before, but the Shadow DOM means Web Components are by definition scoped separately from the rest of the document. This means you can go wild and put anything in the style tags, and it will only be scoped to your component.
customElements . define ( 'my-card' ,
class extends HTMLElement {
constructor () {
super ();
const template = document . getElementById ( 'my-card-template' );
const templateContent = template . content ;
this . attachShadow ({ mode : 'open' }). appendChild (
templateContent . cloneNode ( true )
);
}
}
);
< template id ="my-card-template ">
< style >
: host { /* targets the element wrapping over shadow root */
background : LightSlateGrey;
}
h2 {
color : Green;
}
</ style >
< h2 > This is the card's Header</ h2 >
< slot > This is the card's content</ slot >
</ template >
<!-- later... -->
< my-card > foo</ my-card >
| Requirement | Met? |
|---|---|
| Good Structure | Meh. Web components are verbose. |
| Scoping | Quite well? Unsure how <slot> plays into this, but that's kind of a donut scope like you can do with @scope… |