Listen to or download an audio recording of this episode.
Components: Modular Thinking
Where Your Components Live #
By now, I’m assuming you understand the value of components. And you understand the need to reason about components as part of the “object graph” of your HTML markup.
Perhaps you’re also getting a sense of how starting out with design system components which are largely “atomic” (aka easily reusable and one level below a particular applications’s ubiquitous language) can really help you gain a foothold on how best to build out the interfaces of your websites.
So here’s the next logical question: where exactly do you put your components? Where should they be located within your codebase?
I’m about to trot out that dreaded phrase no one likes to hear an engineer utter:
It depends. 😱
But really, it does. It depends on your particular technology stack. It depends on the type of project you’re working on. And it depends on what your goals are with regard to the use of your design system ultimately.
Many frameworks tell you where to put components and they explain how to create them. For example, Eleventy comes with a new server-oriented component flavor called WebC, one I wrote about here on the blog. Astro, another popular website framework, offers a compelling JSX-like component flavor with zero-JS by default.
Some well-established fullstack frameworks such as Ruby on Rails may not come with a server-side component format out of the box, but community-supported plugins—like the popular ViewComponent gem—can often give you just that.
The framework I personally build and maintain, Bridgetown, does offer a component format out of the box—and it’s quite straightforward to co-locate your Ruby-based template code, your styles, and any client-side JavaScript you might write for a component. It even supports “islands architecture”—as does Eleventy and Astro too for that matter.
I’ll give you a quick explanation of how you’d add the files for a Bridgetown component—not that I’m saying you need to use Bridgetown of course, but because the way that works may help you understand how you might approach this in your particular framework or build system of choice.
Let’s say you want to write a button component. Bridgetown provides a src/_components
folder in every new project, so we could create the following files in there:
button.rb
– the server-side Ruby component definitionbutton.html.erb
– the server-side HTML templatebutton.css
– the stylesheet for the component
And if you need additional JavaScript code associated with the component, yep you guessed it, I’d create a button.js
file as well.
If you’re writing a custom element, just imagine the custom tag name serving as the basename of each of those files—for example, sw-dialog.css
for “Spicy Web Dialog” stylesheet.
Typically what I see is that design systems will break components out into various subfolders which help categorize all the components. You might put buttons, links, etc. in an actions
folder, and dialogs and drawers in an overlay
folder. You could have a blocks
folder or a navigation
folder or a layout
folder (for things like grids). It really all comes down to what makes sense for you and your project/team.
Providing specific guidance at the framework level (or even a no-framework buildless solution) is beyond the scope of this course, but please hop into the Discord channel for CSS Nouveau any time for advice and further resources.
Separate Package? #
On larger projects, or if you’re building a design system you’d like to live on its own and possibly get shared as an open-source project, you’ll definitely want to look into creating a standalone package for the design system. How to create a design system package (shared on NPM, and optionally some other packaging format depending on if you’re building components which integrate with backend stacks like Ruby, Python, Elixir, etc.) is also a good topic for the Discord if you need a nudge in the right direction.
Typically the goal is to provide something you can easily import in applications without requiring a lot of opinionated or non-standard tooling. This is yet another reason I heartily encourage going as “vanilla” as possible on the frontend side of things—if you build a design system atop Tailwind for example, you limit your design system to only working with applications which have already bought into Tailwind. Same goes for many other toolsets.
To a certain degree however, you can get away with anything which is purely for your own DX and can be compiled away. For example, if you use Sass or PostCSS to develop your design system, you can compile those source stylesheets down to exported stylesheets which are 100% vanilla. Then your downstream consumers can import those and not need Sass or PostCSS—except maybe in very specific circumstances.
That does mean you’ll have to be mindful of how you use those tools—for example you can’t simply utilize Sass variables because those variable values would get “baked into” the exported stylesheets. Better to use Sass variables exclusively for things which only benefit your internal styling efforts, and use CSS variables for anything you expect consumers to be able to customize.
Prior Art Can Be Most Enlightening #
It can be really helpful to investigate how projects already out on the market handle their file structure and conventions surrounding where components live and how they get imported and exported. For example, if you’re curious how to build components using a web component library such as Lit, you could peruse the Shoelace repository and see how they do things. Or maybe there’s a popular open source application written in your particular framework of choice—even one that’s provided as a starter kit or demo. You could poke around there and glean some useful insights.
Beware however: the older the example is, the less likely they’re aware of and taking advantage of all of the advances in vanilla CSS I’ve been covering so far in this course. You might come across a theme which is only using Sass variables for everything, or using CSS Modules to “scope” styles so the HTML output is a bunch of hashed class names (eww), or using some grabbag of utility classes everywhere.
I wish I could point you to a definitive list of projects to look at which exhibit all of the modern techniques we’re learning here. In the meantime, the final episode will present you with a simplistic approach to building out some components using just Eleventy and some Liquid HTML templates, along with CSS & JS files which get imported in purely the vanilla way. I’m not even using a bundler like esbuild! This will at least get you a bit farther along to understanding how you might structure things yourself as you build out a full frontend architecture.
Now Here’s Your Chance to DOMinate the Conversation #
Great news! You’re about to learn how to build a vanilla CSS “card component” using a custom element. In this first iteration, we will be forgoing shadow DOM and styling everything with familiar “light DOM” techniques. (Don’t worry, we’ll have plenty of Shadow DOM content on offer in the next episode…in fact everything you learn here builds up to what you’ll eventually learn there.)
Let’s start by reviewing what utilizing a custom element gives us over past approaches.
<div class="card">
#
The huge problem with this should be fairly obvious right out of the gate: you’re building an important component off of a “polluted” global namespace. Your project may be built around a card
class, but guess who else uses card
? Bootstrap, that’s who:
<div class="card">
<div class="card-body">
A simple Bootstrap card.
</div>
</div>
And who knows how many other systems out there also use card
? Sure, the likelihood you’ll pull in Bootstrap’s card here and your own card there and whatever all on a single page is very low. But we live in a new age! We live in the age of components! We want to be able to grab an off-the-shelf component from there and another one from there and shape and mold them into a cohesive, holistic experience. We want to be able to re-use our own components as well from project to project. Say a client hands me a project where they’re already using Bootstrap, but now I want to work on a new page using my own card. I can’t do it if two completely different cards both use <div class="card">
.
<div class="gobble dee gook">
#
Call this the “utility class” solution. If you don’t ever define your card component as card
, you’re safe, right? Just add a bunch of singular utilities into the markup, wrap that in some other sort of component system (React, Vue, maybe something server-side like Ruby’s ViewComponent or Phlex, or Phoenix’s HEEx components, etc.), and you’re done.
Except you’re not done, because utility class frameworks are extremely opinionated, hopelessly viral, and—like “Bates Motel”—it’s really, really hard to leave. For a refresher on the myriad of potential issues with utility classes, read my essay The Three Laws of Utility Classes.
Let’s not go there. There are far better ways to get this done.
<div class="___1wl1eki f1x0m3f5 fl43uef f1eubsub f13qh94s f1ea4fvb f4d9j23 f122n59 fip29mg f1gel1co fguofe8 f1ncbzy4">
#
I’m assuming by now, if you’ve gotten this far, you know how much I loathe build processes which abstract and obfuscate just what the hell is going on. Try debugging this in production, exercising your “Right to Inspect”, or doing virtually anything else that web developers have been accustomed to with semantic markup for the past 30 years. 😂
Time to toss all that CSS Modules/CSS-in-JS build process nonsense out the window. Don’t want it. Don’t need it. Which leads us to:
<li class="my-design-system-card">
#
This is perhaps a slightly better middle ground. First of all, you’re trying to find semantic tags like <article>
, <section>
, <li>
, etc. to build components off of. That’s great. Second of all, you’re making an attempt to “namespace” your classes. Unless your namespace is super generic, the likelihood you’ll conflict with another solution is fairly low.
The downside to this is you’re still stuck in the realm of building components purely out of low-level markup, and dealing with styling within a global scope. Plus so far we’ve just been talking CSS. What if your component needs some JavaScript behavior to go along with it? There’s no obvious solution here, and certainly not a “vanilla” one that’s battle-tested.
<my-design-system-card>
#
Ah, now we’re getting somewhere! This is the modern solution we need. A real custom element that can be upgraded to a full client-side web component with advanced JavaScript interactivity wherever and whenever it’s needed. The component itself should be responsible for defining its interior structure, styling that structure and content accordingly, and providing a styling API for consumers to be able to customize the component in various ways. The only big question then becomes: to shadow or not to shadow?
Who knows what evil lurks in the hearts of proprietary component models? The Shadow DOM knows… #
Apologies for the obscure reference, but I think the term “shadow” is so mysterious and playful I’m surprised there haven’t been a thousand #WebDev memes about it yet. 🤓
This episode is all about building a card without shadow DOM, but the real reason I’m doing that is so you can get a feel for what happens when you don’t use shadow DOM, so that I can then convince you to use shadow DOM. Heh. But first, let’s figure out just what we’re dealing with…
So, Shadow DOM. WTF is it? In essence, shadow DOM—or more precisely a “shadow DOM tree”—is nothing more than a fragment of HTML & CSS which lives in, well, its own tree. Various DOM elements in this tree all hang off of a “shadow root”. This root, obviously, is not <html>
like we’re accustomed to. Normally a tag somewhere on an HTML page is part of a hierarchy like this:
html > body > main > article > header > h2
You always start at <html>
and work your way down the tree. For the sake of argument, let’s call this the light DOM (that’s not strictly-speaking a technical term, but the community has essentially adopted it).
The light DOM is any content which lives outside of a particular shadow root and its direct children. If you don’t use any shadow DOM on your page, that means 100% of your light DOM is just the traditional <html>
tree.
HOWEVER…
Once you start using shadow DOM, some of what is considered “light DOM” actually might end up living within in a parent component’s shadow tree. 😱
At first that’s confusing, but once you start working with it—especially using your browser’s dev tools to inspect the hell out of everything, it becomes more second nature.
So yeah, you can interleave light and shadow DOM all throughout a page. Wild! But I think it’s safe to say most casual users of shadow DOM won’t typically author a multi-level hierarchy of shadow DOMified components.
That’s a bird’s eye view of shadow DOM, and we’ll cover the details of it much more in the next episode.
But first, Dance into the Light… #
I don’t know why I’m on a corny joke kick today, but bear with me.
Let’s start building a card component, which we’ll call spicy-light-card
with only “light DOM” using mostly traditional techniques that should be familiar to anyone with a smidge of CSS knowledge (albeit taking advantage of recent advances in available CSS features).
First, the HTML:
<spicy-light-card>
<header>
<h2>Hello, I'm a Spicy Card</h2>
</header>
<p>Cards are fun because they are nice little units of content often featuring interesting combinations of borders, backgrounds, and drop shadows. You can sprinkle cards on all sort of pages for all sorts of reasons. I love cards!</p>
<footer>
<button id="love">Cards are ♥︎</button>
</footer>
</spicy-light-card>
Nice. Clean. I like it. Now let’s write the stylesheet. We’ll pull in Open Props for some handy-dandy theming variables.
@import "https://unpkg.com/open-props";
spicy-light-card {
display: block;
overflow: hidden;
max-width: 40ch;
padding-block: var(--size-fluid-2);
border: 1px solid var(--stone-5);
border-radius: var(--radius-3);
box-shadow: var(--shadow-3);
}
spicy-light-card > * {
padding-inline: var(--size-fluid-2);
}
spicy-light-card > header {
margin-block-end: var(--size-fluid-2);
}
spicy-light-card > header > *:first-child {
margin-block-start: 0;
}
spicy-light-card > header > *:last-child {
margin-block-end: 0;
}
spicy-light-card > footer {
display: flex;
flex-direction: row-reverse;
margin-block-start: var(--size-fluid-2);
padding-block: var(--size-fluid-1);
background: var(--stone-3);
border-top: 1px solid var(--stone-4);
}
spicy-light-card:has(> footer) {
padding-block-end: 0;
}
All right, so this isn’t so bad, right? There’s surprisingly not a whole lot going on in the stylesheet, and the markup itself is easy to write, easy to use. (Obviously in “component” form, the exact content within the header, the body, etc. would come from the place where the card is utilized. What “framework” you use to render your child content handed off to components is beyond the scope of this lesson.)
Alas, the devil is in the details… #
We can’t commit this, merge a PR, and sign off for the day because this isn’t “a real component” in a “real design system” yet. The design is entirely bespoke and there’s just no customizability. Also it begins to fall apart once you add more advanced content.
For example, let’s add a blockquote to the content:
<spicy-light-card>
<header>
<h2>Uh Oh</h2>
</header>
<blockquote>
<p>Hmm, this doesn't look right.</p>
</blockquote>
</spicy-light-card>
If your blockquotes are typically styled with borders and margin and padding, it gets all goofed up because of the way we’ve forced inline padding for all child content of the card. This is why generally there’s a “body” tag of some type that folks require for markup in a card (and dialogs and a number of other container-style elements).
<spicy-light-card>
<header>
<h2>Uh Oh</h2>
</header>
<div class="spicy-light-card-section">
<blockquote>
<p>This card stands div-ided.</p>
</blockquote>
</div>
</spicy-light-card>
The fact I’m now throwing in a <div>
with a class=
on it should be setting off alarm bells. All of the reasons I’ve been giving throughout this course regarding how we should avoid “white noise” in our markup now becomes crystal clear. That tag is what I might call an “implementation detail” of the component. The component requires an extra tag to wrap body content, but the author of this content does not. It’s superfluous and in the way. It’s messy.
But let’s for the sake of argument say we must use a body content tag. Can we get away with a class-less div? (Styled as spicy-light-card > div
)
<spicy-light-card>
<header>
<h2>Maybe?</h2>
</header>
<div>
<blockquote>
<p>It's better, but not by much.</p>
</blockquote>
</div>
</spicy-light-card>
I still don’t like it, because <div>
is just an annoyingly generic tag. As an author looking at my HTML, <div>
doesn’t convey anything semantically about the fact that this is body content.
What about the <section>
tag? Could we use that? (Styled as spicy-light-card > section
)
<spicy-light-card>
<header>
<h2>This Card Has Been Sectioned Off</h2>
</header>
<section>
<blockquote>
<p>Markup-wise, this definitely looks nicer.</p>
</blockquote>
</section>
</spicy-light-card>
I’d be tempted to say ship it…except there are some weird aspects regarding the semantics of the <section>
tag I’ve never fully settled in my mind. For example, the docs on MDN say: “Sections should always have a heading, with very few exceptions.” and also “If you are only using the element as a styling wrapper, use a <div>
instead.”
Geez. 🤦🏻♂️
OK, let’s step back for a moment and revaluate our options. We’re inside of a spicy-light-card
element. We’re trying to set up a section of the component for success. We tried using a semantic, built-in HTML element and ran into some hurdles. What’s the next logical step?
Ah ha! Another custom element! 😎
<spicy-light-card>
<header>
<h2>This Card Has Been Sectioned Off</h2>
</header>
<spicy-light-card-section>
<blockquote>
<p>Markup-wise, this definitely looks nicer.</p>
</blockquote>
</spicy-light-card-section>
</spicy-light-card>
It’s a bit more verbose, sure, but it says exactly what it means. It’s a section of our card component. No muss, no fuss. And styling it is equally obvious: spicy-light-card > spicy-light-card-section
.
Now let’s address the customizability aspect of this component. It may look great now, but as soon as a new page with a different design comes across your desk (or very likely someone else’s) and you/they need to tweak the card, the last thing in the world you want to be doing is this:
/* in an override stylesheet somewhere */
body#some-page .some.context spicy-light-card {
background: lightyellow;
}
Please, for the love of all that is holy, don’t do this. We know this is a component. We know it’s supposed to be reusable and customizable. So what we really want is a styling API which we can utilize to make new variations. For example, the design calls for a “highlighted” sort of card (hence the light yellow background), so let’s consider making a highlighted card variation. And instead of directly working with style rules and needing to drill down into nested elements and other implementation details, let’s use CSS variables instead!
/* in an override stylesheet somewhere */
spicy-light-card.is-highlighted {
--card-background: lightyellow;
}
Now you can simply add a class="is-highlighted"
to the card on your fancy new page and take advantage of your component’s styling API as the good lord intended. Then later when you decide the component itself should define a highlighted
state, you can add that as a canonical variant of the component, styled via spicy-light-card[highlighted]
within the component stylesheet and used like so:
<spicy-light-card highlighted>
...
</spicy-light-card>
Isn’t this workflow pretty rad? Now imagine applying these principles not only to cards, but to virtually everything across your entire design system and website’s visual language. As Neo might say, whoa.
Let’s revisit our original stylesheet in order to set up a styling API:
@import "https://unpkg.com/open-props";
spicy-light-card {
--card-background: transparent;
--card-max-width: 40ch;
--card-padding: var(--size-fluid-2);
--card-border: 1px solid var(--stone-5);
--card-border-radius: var(--radius-3);
--card-shadow: var(--shadow-3);
--card-footer-padding-block: calc(var(--size-fluid-1) + var(--size-1));
--card-footer-divider: 1px solid var(--stone-4);
--card-footer-background: var(--stone-3);
--card-footer-flex-direction: row-reverse;
display: block;
overflow: hidden;
background: var(--card-background);
max-width: var(--card-max-width);
padding-block: var(--card-padding);
border: var(--card-border);
border-radius: var(--card-border-radius);
box-shadow: var(--card-shadow);
}
spicy-light-card > * {
padding-inline: var(--card-padding);
}
spicy-light-card > header {
margin-block-end: var(--card-padding);
}
spicy-light-card > header > *:first-child {
margin-block-start: 0;
}
spicy-light-card > header > *:last-child {
margin-block-end: 0;
}
spicy-light-card > footer {
display: flex;
flex-direction: var(--card-footer-flex-direction);
margin-block-start: var(--card-padding);
padding-block: var(--card-footer-padding-block);
background: var(--card-footer-background);
border-top: var(--card-footer-divider);
}
spicy-light-card:has(> footer) {
padding-block-end: 0;
}
spicy-light-card-section {
display: block;
}
✨ Live Demo ✨ #
As you can see, we’ve replaced virtually every single hard-coded value in this stylesheet with a token. These tokens themselves inherit from the global set of tokens (aka Open Props in this case). Not only that, but we’ve “hoisted” customization of the component’s implementation details to the top of the component tree. If somebody needs to change the background color of the card’s footer, they don’t need to know that they must style spicy-light-card > footer
. That’s an implementation detail we don’t want to expose, because at some future date we might need to change the tag itself or otherwise tweak the internal structure of the component. Rather, the documentation is crystal clear: change --card-footer-background
and you’re done. We’re happy, you’re happy, everybody’s happy.
Except… 🧐
This requires you to think long and hard about the tokens you expose to the public. Sure, the background of the footer is now set. But what if you want to change the font color of content within the footer? Oops, we don’t have any token for that!
There’s actually another solution for this, but you won’t find it here in the realm of the light DOM. We must “peer into the shadows” to discover a better way. But first, let’s recap what we’ve learned.
>
).
So honestly this is all pretty cool. And it requires zero JS & no build step for any of the markup or stylesheet code. It is 100% compatible with every web framework and toolkit on the planet. (And if that’s ever not the case, that’s the framework’s fault.)
The best news of all though is it’s future compatible. We’re not merely talking about compatibility among tools today. We’re talking a year from now. Two years from now. Five years from now. Ten years from now. Twenty years from now!
Yes, I am telling you this card component will work just fine twenty years from now.
How do I know this? Because any card component written twenty years ago (that was 2003 for those keeping score) using 100% vanilla HTML & CSS will still work fine today. Heck, today’s web browsers can still display the first web page ever published! How crazy is that?!
I don’t know about you, but I really don’t feel like reinventing cards, or million-dollar buttons, or whatever every couple of years whenever there’s a slick new framework in town with CSS-in-Rust or whatever the cool kids are doing these days. I just want to freaking write 🧑🏼💻 some HTML and some CSS, clock ⏰ out for the evening, and go ride my scooter 🛴 or listen to a podcast 🎧. I don’t ask for much! 😅
The Need for True Encapsulation #
Here we are, feeling all warm and fuzzy inside, basking in the glow of our vanilla card component, when who should come along but yours truly to rain on the parade. My most sincere apologies! 😉
You see, there’s a problem with our component.
Depending on the particular page its placed on—perhaps even where on the page it happens to be placed—all sorts of aspects of it might look completely different. Goofy. Weird. Bad.
Sure, we did our best to scope our component styles via fairly-decently-scoped selectors, but outside selectors might screw it all up. Our component is not truly encapsulated. For smaller projects, solo projects, projects with tight-knit teams, projects where everything on every page is meticulously crafted, strict encapsulation may not be required. (In fact, attempts to formally introduce encapsulation might, admittedly, be annoying.)
But on larger projects, with larger teams, or when needing to write components which are easily reusable (maybe you’re writing a component you’d like to publish as open source or otherwise distribute to the wider world), you really, really, really want encapsulation. Trust me on this.
Also, “cards” are by and large not that complex. And they’re mainly just there for visual reasons. Other components are much more complex, involving many moving parts and subcomponents and states and interactive behaviors and…and…and. Having to worry about the interplay between styling a component’s implementation details and refraining from messing up the styles of child content (or having that content interfere with the component itself) can be a real hassle. It’s why people end up reaching for more abstracted tools like CSS-in-JS or utility classes or whattnot. You wince and take on a bucket-load of upfront complexity to try to mitigate some of the darned annoying issues which can crop up later on. Comes with the territory, right?
These sorts of issues are why certain people claim that vanilla CSS “doesn’t scale” and that best practices “don’t work” and all that chatter.
Well? Are they right? 🤔
Or…has the platform actually moved forward and folks don’t realize it… 😏
Hmm…
If only…there was a way to build truly encapsulated components for the web, where markup structure and JavaScript behavior is safely cordoned off from the wider world of the full webpage.
If only…you could style your component’s implementation details separately from however child content might be styled, and ensure parent content & styling has minimal negative impact on your component.
If only…you could reason about your components as you would objects in any object-oriented system, building advanced APIs which offer “contracts” between object vendors and object consumers.
If only…you could server-render a component in such a way that its implementation details are shipped with the component while at the same time any child content supplied to the component lives alongside it but not intermingled with it.
If only…we had…
(drumroll please) 🥁