Listen to or download an audio recording of this episode.
Zooming Out to the Design System Production Level
The Three Legs of the Stool #
Let’s review the basics of all that we’ve learned thus far.
The web is made of up three core languages. (Well, that plus the HTTP(S) protocol, but that’s a topic for another time!) Each language serves a different purpose and—just as importantly—offers a different reliability and compatibility story.
You could think of these languages as three legs of a stool. Compromise any one of them in favor of a different one, and whoosh, you’re suddenly on the floor!
The most reliable, compatible—and accessible!—language is HTML. The browser always downloads, processes, and renders HTML first. HTML is the substrate of everything else that happens on a webpage. That’s why we like to share the rule of thumb that if you can express something purely in HTML, you probably should.
The next language in the list is CSS. Whereas HTML is responsible for the structure and the content of your webpage, CSS is responsible for the presentation and the appearance of your webpage. With the rate of change we see these days in CSS, it can be a little less compatible, a little less reliable. Some tricks are new and might break things in older browsers, or still work a little differently from browser to browser. Thankfully, the evolution of CSS has come to match the rapid pace of evergreen browser evolution (which itself is a major departure from past eras of the web!). So, that awesome CSS trick you just picked up? In a year or so, you might just be able to deploy it to production.
Finally, our third language is JavaScript. It’s the most fragile, most likely to break in unexpected ways. But it also offers great power and flexibility. We look to JS code to progressively enhance our webpage with interactive behavior. We look to JS sometimes to polyfill features in specific browsers which might not yet offer native support. We also rely on JavaScript to help us build various kinds of advanced components, to coordinate work across a variety of components, and to provide more engaging user experiences than our HTML-only baseline. Remember, JavaScript isn’t the enemy! We’re not trying to “do everything” without JS, nor are we expecting that our sites will always run flawlessly without JS executing. The tenets of progressive enhancement are literally in the name—you attempt to do a reasonable job of providing an acceptable feature baseline without JavaScript…and then polish everything to a shining gem with the “spices” of an add-on frontend runtime.
HTML, then CSS, then JavaScript. It’s a methodology, yes—but it’s also now a web-native packaging technology. Prior to the arrival of web components, there wasn’t really an official way to say “here’s a wad of HTML/CSS/JS you can reason about as a singular entity.” But now it’s totally possible to define and examine the disparate parts of your website as represented by a tree of self-sufficient components.
This technically mirrors what is actually happening in the browser as it constructs the DOM. As HTML is translated into a live object graph, you can think of each element in the graph as essentially a component. A heading. A list. A button. A link. And the good news is, we now have the ability to create our own custom elements, which are “higher-level” components sitting right there in the DOM, alongside or wrapping many default elements. This makes them very easy to debug, to share, and to integrate into various projects regardless of the particulars of your tech stack.*
(* Unless you’re using React. 🥴 C’mon, ship your official support already! 🤪)
The Rise of the Evergreen Design System #
What might we call such an assemblage of components, along with the relevant base and semantic tokens, theming options, and stylistic decision-making we construct as a fundamental marriage of design & development, form & function? We call this a design system. If the “legs of the web development stool” are HTML, CSS, and JavaScript, then the “seat of the web development stool” is the design system. We construct our design system so that we have a fully stabilized structure upon which to build new features for our website. We also want to ensure the website is “comfortable” to use and to extend over time.
A design system is never static. It’s constantly changing and evolving to serve the needs of users and developers alike. It’s also providing some much needed guardrails, so that Features A, B, and C worked on years ago and Features D and E being worked on tomorrow don’t feel like totally different and bewildering experiences (for either developers or users!). Like browsers themselves, you might say that design systems offer a bit of an “evergreen” application experience. However, operating on the theory of “pace layers”, your design system doesn’t have to move fast and furiously like the rest of your software architecture. Making changes a bit more slowly, a bit more formally, a bit more deliberately—those are features, not bugs.
And specific to this course’s primary thesis: while you can build design systems using a variety of tooling and frameworks out on the market today, my experience has been that it’s consistently the right call to use vanilla tech whenever and wherever possible.
It’s better to use CSS variables for design tokens—or use a tool which will automate converting your token definitions to CSS variables regularly.
It’s better to “name things” and use ubiquitous language from the atomic component level all the way up into production feature rollouts.
It’s better to build design system components using custom elements and other web-native APIs for future-proofing and wide compatibility across stacks.
It’s better to construct finely-hewn styling APIs so that downstream consumers can make non-breaking, context-specific changes in ways which have been “blessed” by the design system authors.
It’s better to leverage the power of full encapsulation when and where needed using the power of shadow DOM and its public/private template composition format.
All of these architectural decisions will ultimately make your products better because you’re bringing all of the features of the modern web and all of the capabilities of modern browsers directly to bear on your code. These are important capabilities which definitely aren’t going away any time soon. The web has proven itself time and time again to offer the incredible selling point that what you build today will still work tomorrow. And the next day. And the next day. And the next day after that.
And all of the features we’ve looked at throughout this course are simply going to get better with age—not fall into dead-end technical debt or unsupported experimental toolchains.
Avoiding the Append-Only Apocalypse #
Website projects over time may trend towards near-catastrophic ruin due to mushrooming code size, and CSS in particular can quickly come to feel unmanageable—if there’s no sensible architecture in place. Over the years, many different methodologies have been suggested for CSS to make it manageable by many different teams and organizations. Some of them have found a certain degree of success, but I must contend that CSS itself has changed so dramatically in just the past few years—and we have completely new styling technologies available to us such as CSS variables, shadow DOM, and grid layout—that it necessitates a reimagining of how we can structure and maintain our CSS going forward.
In other words, it’s not only possible that past methodologies are ill-suited to the stacks of today or tomorrow, it’s rather likely they’re holding you back from embracing the technology now available to you.
Even today, some folks will tell you that “vanilla CSS is unmanageable” and you must use [insert pet framework/methodology here]. Scratch the surface however, and you may discover these same people don’t actually understand what’s possible today at the core technology level.
That’s why you constantly need to research the state of the art. Don’t simply take somebody’s word for it. Don’t even take my word for it! Remember the mantra: CodePen or it didn’t happen.
What we need to do is figure out how to slice our CSS up into many different layers and then which CSS capabilities we might leverage at each level. This way, as we build out a new application feature, the likelihood we’ll need to come up with a bunch of one-off “snowflake” CSS is heavily minimized, because we can always drop down a layer to leverage more and more flexible primitives available to us from our design system.
Instead of a completely “flat” conception of every feature, you can break each feature up into a hierarchical process:
- This feature uses design system components A, B, and C, along with a new single-use component D which in turn uses design system components E and F.
- This feature requires a tweak to the typography for design purposes, so we’ll simply override some of the design tokens as we call out to a couple of components.
- We intend to encapsulate the entire feature inside a new component, so that any feature-specific styles (mostly overrides) live within this feature component.
- Does this feature need to be loaded with the main site bundle? No? Then let’s encapsulate it in an “island” so our HTML, CSS, and JavaScript only needs to load when the user actually accesses the specific page where the feature lives.
Obviously some features/components/styles will always need to be preloaded within your website’s global runtime. You’ll use a button or a card or a grid layout all over the place, so they must be included in your main bundle. However, many features you can encapsulate to the specific page(s) where they get used. Generally speaking, any new styles you add should live within those encapsulations.
This is the solution to append-only technical debt hell. Every time you add new styles—every single time—on a well-established project, ask yourself:
- Is this feature-specific? If so, am I scoping/encapsulating the styles to that particular feature?
- Is this page template-specific? If so, am I scoping/encapsulating the styles to this particular page template?
- Is this a major modification to an existing design system component? If so, should that be done as an upgrade to the component itself? If not, perhaps it’s a whole new design system component? Should I then plan out the work so that it’s polished to the level of a real design system component and isn’t merely single-feature-specific?
Remember, the three huge knocks against “append-only stylesheets” are:
- Performance and DX. If your grab bag of global stylesheets are many thousands of lines of code, yikes!
- Spooky action at a distance. Oh no! I changed this style rule to fix this page here, and it totally broke five other pages over there!
- Annoying continuity issues. This color isn’t quite that color. The padding here isn’t quite that padding there. These font sizes are slightly different. That border radius is too rounded!
Guess what? We don’t need something like utility classes to fix all of these problems at once. In fact, some of the aforementioned problems are exacerbated by that methodology!
What we truly need is a solid library of design tokens as CSS variables, component-level and feature-level encapsulation, and islands architecture—the latter of which will simultaneously help with JavaScript performance and code health as well.
Creating Your Own Methodology #
This is the “When You’re Ready, You Won’t Have To (Dodge Bullets)” phase of your CSS journey.
Given all I’ve been saying, it might sound like I’m “against” established CSS methodologies such as BEM or SMACSS (old-school but still nifty!) or CUBE. It’s not that I’m against any particular CSS methodology (OK, maybe I’m particularly biased against BEM at this point…), but rather that I’m much more in favor of people/projects/teams creating their own methodologies.
The consistency of your methodology is what makes it so powerful. What is the difference between all of these?
<div class="card card--highlighted">
<div class="card__header"></div>
<div class="card__body"></div>
<div class="card__footer"></div>
</div>
<div class="card is-highlighted">
<div class="card-header"></div>
<div class="card-body"></div>
<div class="card-footer"></div>
</div>
<div class="[ card ] [ section box ] [ bg-base color-highlighted ]">
<header class="[ bg-highlighted color-inverse ]">
</header>
<div class="[ flow ]"></div>
<footer class="[ bg-actions ]">
</footer>
</div>
To start with, the difference is if y’all like it or not! Programmers often like to cast personal preference as dispassionate technical decision-making. Let’s face it: a lot of folks choose BEM because they like it. And a lot of folks reject BEM because they hate it. There’s no shame in that.
Beyond personal preference however, the place where you should start to make informed and more objective choices is what makes the most semantic sense in the HTML markup. Ultimately what makes any methodology good or bad is how consistently you’re able to use it and how useful it is for achieving your goals on your project as well as offers the best possible use of accessible & semantic native & custom HTML elements. (And please, if we can get far, far away from div tag soup, so much the better!)
What I really want to you feel is the permission to create your own methodology, and the strength of conviction to see it through as a project grows and evolves. This methodology should match the way you’re structuring your codebase, your design system, and your components.
For example, this wouldn’t make much sense:
<app-card class="card">...</app-card>
If you’re using custom elements, you no longer need a class to identity your component and hang styles off of it. But what about variations? You could still do this:
<app-card class="is-highlighted">...</app-card>
But perhaps that only makes sense if you have a certain “highlighted” style that crossed a whole bunch of different components. If it’s only for the card, you could make it an attribute/prop of the card component:
<app-card highlighted>...</app-card>
Now this is what I’d prefer. Not just because I like how clean it is, but also because I’m allowing my methodology to flow out of the semantic nature of the markup.
Let’s look at another example, a real one from the demo we’ll be looking at in the next episode. I wanted to add an icon to an h2
at the top of a cell, so I created a “variation attribute” and styled accordingly:
<bd-dashboard-cell>
<h2 with-icon>
Group Activities
<bd-icon>...svg...</bd-icon>
</h2>
...
</bd-dashboard-cell>
So this is an interesting question. How do you “extend” a built-in HTML element like a heading with a styled variant? Do you use a class? Do you use an attribute? How do you name it? There are so many ways to do it:
<h2 class="has-icon">
<h2 class="icons-flex">
<h2 class="[ flex-icon ]">
<h2 has-icon>
<h2 with-icon>
<with-icon><h2>...</h2></with-icon>
(the compositional approach! don’t knock it ‘till you try it!)
Again, I will have my go-to preferences and in some cases might be able to argue the technical merits of one over another. But at the end of the day, I encourage you to try a wide variety of different approaches and get a feel for what your HTML markup could look like and what your CSS could look like. The important thing here is that you don’t bypass this step! Your job as you grow in your career and your technical acumen is that you don’t blindly accept the specific methodologies people send your way.
That’s what makes me particularly sad about the whole utility class phenomenon. In utility class land, you’d just do something like this:
<h2 class="flex gap-4 items-end justify-between">...</h2>
Not only is that a s**t-ton of boilerplate to lug around every time you want a heading with an icon (god forbid you don’t componentize it and then realize you need to change the gap of all of those later!), but this conveys nothing to the reader of the markup. It doesn’t say “this heading lets you display an icon”. It says “this heading has a flexbox applied with a gap of 4 horizontally aligned to the end of the container and with space placed between flexbox children”. In plain English…what the hell is all that supposed to mean?!
Whoops. The naming step was bypassed, and—even worse—some people consider bypassing that step a virtue. I consider it a detriment.
If you’re applying a flexbox layout to a heading so that you can get an icon nicely placed there, then why can’t you simply say that? Communicating author intent is vitally important to producing understandable and maintainable markup and styling over time. Avoiding the time and hassle of “naming things” isn’t whiz-bang smart. It’s cutting corners, and it’s—dare I say—lazy. And it’s disrespectful to the people who might come along long after you’re gone, wondering just what the hell you were trying to accomplish. I dread the moment I have to hop aboard a new project and now need to spend hours just wondering why any of the UI was written the way it was written. 😕
Naming Things is Fun #
Some days I feel like 50% of the work I do on the frontend of a website project is naming things. The cool part is: once you name a thing, you get to define the shape of the thing you’ve named, you get to communicate the intent behind the naming of it, and you get to fit that name into a rich vocabulary of well-named things across your project and your design system.
For example, right now my with-icon
variation only gets applied to h2
tags:
h2[with-icon] {
display: flex;
gap: var(--size-3);
align-items: end;
justify-content: space-between;
}
But maybe down the road, I’ll discover I want this to apply to several heading levels. No worries! I’ll simply expand the cascade:
:is(h2,h3)[with-icon] {
display: flex;
gap: var(--size-3);
align-items: end;
justify-content: space-between;
}
And then maybe at some point there will be a need to tweak the gap between the text and the icon, so we could turn this into more of a “component” with its own gap token:
:is(h2,h3)[with-icon] {
--interior-gap: var(--size-3);
display: flex;
gap: var(--interior-gap);
align-items: end;
justify-content: space-between;
}
Wait, why did we name that interior-gap
? Because that’s a name we’ve used in similar circumstances in other parts of our design system, and this brings consistency and predictability. It would be bad if across several use cases/components/features, you find:
--interior-gap
--gap-interior
--inside-spacing
--the-space-in-between
- etc.
No design system will ever be perfect of course, and there will always be a little bit of a discrepancy here and there. What’s rad though is if you need to maintain backwards-compatibility across projects from within a single design system, you can always deprecate an older token and use it conditionally!
/* new token: */
--inner-spacing: var(--size-3);
/* old --interior-gap token is deprecated */
--_inner-spacing: var(--interior-gap, var(--inner-spacing));
gap: var(--_inner-spacing);
What we’re doing here is creating a secondary “private” variable (denoted by the use of _
), and setting it to --interior-gap
—however, by default we’re no longer setting that value, so it rolls over to use --inner-spacing
. Then if we use --_inner-spacing
later in the stylesheet, we’ll get either --inner-spacing
or -interior-gap
’s value (which might still be getting set by a consumer) depending on what’s in use.
CSS variables rule!
The Era of the Design System—Not Just for Large Enterprises! #
One pseudo-complaint you might come across out in the wild is that it’s all very well and good for “large enterprises” to spend a huge amount of time and resources building out elaborate design systems. I’ve also heard that the ability for web components in particular to fit into a variety of different architectures and stacks makes them great for enterprises, but us “mere mortals” working on our own side projects or small biz projects or whatever don’t need any of that! Grab Svelte, shoehorn some “scoped” CSS into some grab bag of project-specific files, and boom, done.
I think this is a category error.
It’s true that normally I believe we should be wary of adopting “Big Tech” technology if we’re working at any scale other than “Big Tech”. Facebook’s problems aren’t your problems. (Unless you work at Meta.)
But framework-agnostic design systems, web components, hierarchical multi-tier architectures, and other related frontend methodologies aren’t just good ideas for enterprises. They’re good ideas for everybody.
- We get to write code that’s modular.
- We get to write code that’s future-proof.
- We get to write code in a way that works regardless of where we might find ourselves in our careers down the road.
- We get to write code that’s closer to the metal of the web, which invariably means we’re less likely to fall prey to the whims of framework authors (just look at all the consternation right now over Next.js and React Server Components!) and ever-changing coolness factors (Svelte might be “cool” now but will it still be cool in five years? Ten?)
- In some cases, we even get major performance or accessibility wins.
I think all of those points make the extra upfront effort worth it. Sure, you can simply reach for an off-the-shelf solution in the form of a Bootstrap template or a Tailwind-based component library, and build something RIGHT TF NOW that looks and feels “OK”.
But you’re a professional. You’re an artist. You take pride in your craft. You want to get the job done right the first time. Measure twice, cut once. And you want to hone skills which will stand the test of time and go with the grain of the web, not against it.
Further Reading #
Before I close out this elongated jibber-jabber concerning design systems and thus get on with a bunch of genuine code examples in the final episode of Vanilla Has Never Tasted So Hot, I have some additional reading and homework for you to do.
Brad Frost’s “Design System Ecosystem” Mega-Post #
Seriously, this one post is like a mini-course all on its own. Some of it may go way over your head because Brad’s often working on Big Enterprise Systems™️, but as I keep saying, even if your project is way simpler you can still pick and choose from the variety of techniques on display here.
Eric Meyer’s “Blinded by the Light DOM” #
I really enjoyed reading through Eric’s step-by-step process of finding a path away from past “build a widget that does a thing on this webpage” techniques from yesteryear and towards a new web-component-first mental model—even when eschewing shadow DOM and its more unconventional feature set. If you’re exciting about building and styling web components but are still a little nervous stepping away from a pure light DOM methodology, this is a great resource.
Kevin Pennekamp went “back to CSS-only after years of SCSS” #
I thought this was a great overview of why you might be able to dump your preprocessor of choice and experiment with new techniques afforded by today’s vanilla CSS. I particularly thought Kevin’s “class utilities” (not to be confused with utility classes!) technique was rather ingenious. I haven’t had a chance to try it yet—maybe you will!
Rob Eisenberg on “The Many Faces of a Web Component” #
If you still find yourself a bit confused about how all the various APIs related to web components fit together, and how they fit together with many of the other APIs and protocols and methodologies and best practices around web development in general, this is a very welcome primer.
And in a two-for-one-deal, Rob also wrote a 2023 State of Web Components deep-dive into all current and future specs you might want to learn about in this space. I hope it helps to show just how far you can go with this technology and why frontend / fullstack “Web Component Specialist” may actually be a viable job title before too long.
What Will You Build? #
If there’s a particular methodology or set of techniques you feel particularly inspired to try out after going through this course or reading the supplementary materials linked above, I encourage you to go for it! Try building a small demo project which will allow you to flex your muscles. It could be a new portfolio website, or a gallery or presentation, or an essay with lots of interactive diagrams and charts, or the venerable todo list, or an events calendar, or an app to help you catalog your large vintage record collection.
Whatever it is, see how much you can push the limits of what you currently know by incorporating these new techniques. We’d love to hear all about your experiments in The Spicy Web Discord!
In the next and final episode, I will demonstrate how I built the start of a business dashboard featuring charts, messages, events, a responsive sidebar, and more. It’s not a starter kit for you to clone, but just one possible example of how I tried to do as much as possible with “everything vanilla” plus a tiny bit of assistance from Eleventy and Liquid templates.