Archive for January, 2021

Options for styling web components

When I released emoji-picker-element last year, it was my first time writing a general-purpose web component that could be dropped in to any project or framework. It was also my first time really kicking the tires on shadow DOM.

In the end, I think it was a natural fit. Web components are a great choice when you want something to be portable and self-contained, and an emoji picker fits the bill: you drop it onto a page, maybe with a button to launch it, and when the user clicks an emoji, you insert it into a text box somewhere.

Screenshot of an emoji picker with a search box, a row of emoji categories, and a grid of emoji

What wasn’t obvious to me, though, was how to allow users to style it. What if they wanted a different background color? What if they wanted the emoji to be bigger? What if they wanted a different font for the input field?

This led me to researching all the different ways that a standalone web component can expose a styling API. In this post, I’ll go over each strategy, as well as its strengths and weaknesses from my perspective.

Shadow DOM basics

(Feel free to skip this section if you already know how shadow DOM works.)

The main benefit of shadow DOM, especially for a standalone web component, is that all of your styling is encapsulated. Like JavaScript frameworks that automatically do scoped styles (such as Vue or Svelte), any styles in your web component won’t bleed out into the page, and vice versa. So you’re free to pop in your favorite resets, like so:

* {
  box-sizing: border-box;
}

button {
  cursor: pointer;
}

Another impact of shadow DOM is that DOM APIs cannot “pierce” the shadow tree. So for instance, document.querySelectorAll('button') won’t list any buttons inside of the emoji picker.

Brief interlude: open vs closed shadow DOM

There are two types of shadow DOM: open and closed. I decided to go with open, and all of the examples below assume an open shadow DOM.

In short, closed shadow DOM does not seem to be heavily used, and the drawbacks seem to outweight the benefits. Basically, “open” mode allows some limited JavaScript API access via element.shadowRoot (for instance, element.shadowRoot.querySelectorAll('button') will find the buttons), whereas “closed” mode blocks off all JavaScript access (element.shadowRoot is null). This may sound like a security win, but really there are plenty of workarounds even for closed mode.

So closed mode leaves you with an API surface that is harder to test, doesn’t give users an escape hatch (see below), and doesn’t offer any security benefits. Also, the framework I chose to use, Svelte, uses open mode by default and doesn’t have an option to change it. So it didn’t seem worth the hassle.

Styling strategies

With that brief tangent out of the way, let’s get back to styling. You may have noticed in the above discussion that shadow DOM seems pretty isolated – no styles go in, no styles get out. But there are actually some well-defined ways that styles can be tweaked, and these give you the opportunity to offer an ergonomic styling API to your users. Here are the main options:

  1. CSS variables (aka custom properties)
  2. Classes
  3. Shadow parts
  4. The “escape hatch” (aka inject whatever CSS you want)

One thing to note is that these strategies aren’t “either/or.” You can happily mix all of them in the same web component! It just depends on what makes the most sense for your project.

Option 1: CSS variables (aka custom properties)

For emoji-picker-element, I chose this option as my main approach, as it had the best browser support at the time and actually worked surprisingly well for a number of use cases.

The basic idea is that CSS variables can actually pierce the shadow DOM. No, really! You can have a variable defined at the :root, and then use that variable inside any shadow DOM you like. CSS variables at the :root are effectively global across the entire document (sort of like window in JavaScript).

Here is a CodePen to demonstrate:

Early on, I started to think of some useful CSS variables for my emoji picker:

  • --background to style the background color
  • --emoji-padding to style the padding around each emoji
  • --num-columns to choose the number of columns in the grid

These actually work! In fact, these are some of the variables I actually exposed in emoji-picker-element. Even --num-columns works, thanks to the magic of CSS grid.

However, it would be pretty awful if you had multiple web components on your page, and each of them had generic-sounding variables like --background that you were supposed to define at the :root. What if they conflicted?

Conflicting CSS variables

One strategy for dealing with conflicts is to prefix the variables. This is how Lightning Web Components, the framework we build at Salesforce, does it: everything is prefixed by --lwc-.

I think this makes sense for a design system, where multiple components on a page may want to reference the same variable. But for a standalone component like the emoji picker, I opted for a different strategy, which I picked up from Ionic Framework.

Take a look at Ionic’s button component and modal component. Both of them can be styled with the generic CSS property --background. But what if you want a different background for each? Not a problem!

Here is a simplified example of how Ionic is doing it. Below, I have a foo-component and a bar-component. They each have a different background color, but both are styled with the --background variable:

From the user’s perspective, the CSS is quite intuitive:

foo-component {
  --background: hotpink;
}

bar-component {
  --background: lightsalmon;
}

And if these variables are defined anywhere else, for instance at the :root, then they don’t affect the components at all!

:root {
  --background: black; /* does nothing */
}

Instead, the components revert back to the default background colors they each defined (in this case, lightgreen and lightblue).

How is this possible? Well, the trick is to declare the default value for the variable using the :host() pseudo-class from within the shadow DOM. For instance:

/* inside the shadow DOM */
:host {
  --background: lightblue; /* default value */
}

And then outside the shadow DOM, this can be overridden by targeting the foo-component, because it trumps :host in terms of CSS specificity:

/* outside the shadow DOM */
foo-component {
  --background: hotpink; /* overridden value */
}

It’s a bit hard to wrap your head around, but for anyone using your component, it’s very straightforward! It’s also unlikely to run into any conflicts, since you’d have to be targeting the custom element itself, not any of its ancestors or the :root.

Of course, this may not be enough reassurance for you. Users can still shoot themselves in the foot by doing something like this:

* {
  --background: black; /* this will override the default */
}

So if you’re worried, you can prefix the CSS variables as described above. I personally feel that the risk is pretty low, but it remains to be seen how the web component ecosystem will shake out.

When you don’t need CSS variables

As it turns out, CSS variables aren’t the only case where styles can leak into the shadow DOM: inheritable properties like font-family and color will also seep in.

For something like fonts, you can use this to your advantage by… doing nothing. Yup, just leave your spans and inputs unstyled, and they will match whatever font the surrounding page is using. For my own case, this was just one less thing I had to worry about.

If inheritable properties are a problem for you, though, you can always reset them.

Option 2: classes

Building on the previous section, we get to the next strategy for styling web components: classes. This is another one I used in emoji-picker-element: if you want to toggle dark mode or light mode, it’s as simple as adding a CSS class:

<emoji-picker class="dark"></emoji-picker>
<emoji-picker class="light"></emoji-picker>

(It will also default to the right one based on prefers-color-scheme, but I figured people might want to customize the default behavior.)

Once again, the trick here is to use the :host pseudo-class. In this case, we can pass another selector into the :host() pseudo-class itself:

:host(.dark) {
  background: black;
}
:host(.light) {
  background: white;
}

And here is a CodePen showing it in action:

Of course, you can also mix this approach with CSS variables: for instance, defining --background within the :host(.dark) block. Since you can put arbitrary CSS selectors inside of :host(), you can also use attributes instead of classes, or whatever other approach you’d like.

One potential downside of classes is that they can also run into conflicts – for instance, if the user has dark and light classes already defined elsewhere in their CSS. So you may want to avoid this technique, or use prefixes, if you’re concerned by the risk.

Option 3: shadow parts

CSS shadow parts are a newer spec, so the browser support is not as widespread as CSS variables (still pretty good, though, and improving daily).

The idea of shadow parts is that, as a web component author, you can define “parts” of your component that users can style. CSS Tricks has a good breakdown, but the basic gist is this:

/* outside the shadow DOM */
custom-component::part(foo) {
  background: lightgray;
}
<!-- inside the shadow DOM -->
<span part="foo">
  My background will be lightgray!
</span>

And here is a demo:

I think this strategy is fine, but I actually didn’t end up using it for emoji-picker-element (not yet, anyway). Here is my thought process.

Downsides of shadow parts

First off, it’s hard to decide which “parts” of a web component should be styleable. In the case of the emoji picker, should it be the emoji themselves? What about the skin tone picker, which also contains emoji? What are the right boundaries and names here? (This is not an unsolvable problem, but naming things is hard!)

To be fair, this same criticism could also be applied to CSS variables: naming variables is still hard! But as it turns out, I already use CSS variables to organize my code internally; it just jives with my own mental model. So exposing them publicly didn’t involve a lot of extra naming for me.

Second, by offering a ::part API, I actually lock myself in to certain design decisions, which isn’t necessarily the case with CSS variables. For instance, consider the --emoji-padding variable I use to control the padding around an emoji. The equivalent way of doing this with shadow parts might be:

emoji-picker::part(emoji) {
  padding: 2em;
}

But now, if I ever decide to define the padding some other way (e.g. through width or implicit positioning), or if I decide I actually want a wrapper div to handle the padding, I could potentially break anyone who is styling with the ::part API. Whereas with CSS variables, I can always redefine what --emoji-padding means using my own internal logic.

In fact, this is exactly what I do in emoji-picker-element! The --emoji-padding is not a padding at all, but rather part of a calc() statement that sets the width. I did this for performance reasons – it turned out to be faster (in Chrome anyway) to have fixed cell sizes in a CSS grid. But the user doesn’t have to know this; they can just use --emoji-padding without caring how I implemented it.

Finally, shadow parts expand the API surface in ways that make me a bit uncomfortable. For instance, the user could do something like:

emoji-picker::part(emoji) {
  margin: 1em;
  animation: 1s infinite some-animation;
  display: flex;
  position: relative;
}

With CSS shadow parts, there are just a lot of unexpected ways I could break somebody’s code by changing one of these properties. Whereas with CSS variables, I can explicitly define what I want users to style (such as the padding) and what I don’t (such as display). Of course, I could use semantic versioning to try to communicate breaking changes, but at this point any CSS change on any ::part is potentially breaking.

In (mild) defense of shadow parts

That said, I can definitely see where shadow parts have their place. If you look at my colleague Greg Whitworth’s Open UI definition of a <select> element, it has well-defined parts for everything that makes up a <select>: the button, the listbox, the options, etc. In fact, one of the main goals of the project is to standardize these parts across frameworks and specs. For this kind of situation, shadow parts are a natural fit.

Shadow parts also increase the expressiveness of the user’s CSS: the same thing that makes me squeamish about breaking changes is also what allows users to go hog-wild with styling any ::part however they choose. Personally, I like to restrict the API surface of the code that I ship, but there is an inherent tradeoff here between customizability and breakability, so I don’t believe there is one right answer.

Also, although I organized my own internal styling with CSS variables, it’s actually possible to do this with shadow parts as well. Inside the shadow DOM, you can use :host::part(foo), and it works as expected. This can make your shadow parts less brittle, since they’re not just a public API but also used internally. Here is an example:

Update: as of this writing (May 2022), this is supported in Firefox and Safari 15.4+. As for Chrome, it should land in version 103. In the meantime, another option is the attribute selector: [part="foo"].

Once again: these strategies aren’t “either/or.” If you’d like to use a mix of variables, parts, and classes, then by all means, go ahead! You should use whatever feels most natural for the project you’re working on.

Option 4: the escape hatch

The final strategy I’ll mention is what I’ll call “the escape hatch.” This is basically a way for users to bolt whatever CSS they want onto your custom element, regardless of any other styling techniques you’re using. It looks like this:

const style = document.createElement('style')
style.innerHTML = 'h1 { font-family: "Comic Sans"; }'
element.shadowRoot.appendChild(style)

Because of the way open shadow DOM works, users aren’t prevented from appending <style> tags to the shadowRoot. So using this technique, they always have an “escape hatch” in case the styling API you expose doesn’t meet their needs.

This strategy is probably not the one you want to lean on as your primary styling interface, but it is kind of nice that it always exists. This means that users are never frustrated that they can’t style something – there’s always a workaround.

Of course, this technique is also fragile. If anything changes in your component’s DOM structure or CSS classes, then the user’s code may break. But since it’s obvious to the user that they’re using a loophole, I think this is acceptable. Appending your own <style> tag is clearly a “you broke it, you bought it” kind of situation.

Conclusion

There are lots of ways to expose a styling API on your web component. As the specs mature, we’ll probably have even more possibilities with constructable stylesheets or themes (the latter is apparently defunct, but who knows, maybe it will inspire another spec?). Like a lot of things on the web, there is more than one way to do it.

Web components have a bit of a bad rap, and I think the criticisms are mostly justified. But I also think it’s early days for this technology, and we’re still figuring out where web components work well and where they need to improve. No doubt the web components of 2021 will be even better than those of 2020, due to improved browser specs as well as the community’s understanding of how best to use this tool.

Thanks to Thomas Steiner for feedback on a draft of this blog post.

2020 book review

Like most people, 2020 was a weird year for me. I found myself retreating into the cloistered comfort of my living room, playing a lot more videogames and doing less reading.

Maybe I just needed the escapism, or maybe reading itself felt more stressful when all the headlines were so dire. Either way, my Switch reports that I spent hundreds of hours on immersive games like Zelda: Breath of the Wild, Stardew Valley, and Octopath Traveler.

Those are all great games! But since I’ve made a tradition of it, here is a (somewhat shorter) list of the books I read and enjoyed in 2020.

Quick links

Fiction

Nonfiction

Fiction

The Masters of Solitude by Marvin Kaye and Parke Godwin (1978)

I’ve mentioned it before, but post-apocalyptic fiction is one of my favorite genres. (I’m a natural pessimist, I guess!) This book is a bit of a hidden gem – it’s out of print, and if you check the reviews on Goodreads, you’ll see lots of comments saying that it’s a great book that almost nobody’s ever heard of. It’s also my top pick for 2020.

The book takes place hundreds of years in the future, focusing on a religious conflict between now-dominant Wiccans and minority Christians in present-day America. There are lots of fun, subtle references to places on the east coast: “Shando” I assume is Shenandoah, “Charzen” is maybe Charleston, “Mrika” is America (but not the whole continent, more of a “Holy Roman Empire” kind of thing). The book also keeps you at arm’s length by not revealing too many of its secrets early on.

The mythology and world-building are pretty rich here, and I found myself sucked in even without (yet) checking out the second book in the series. For a moment, you can even forget that it’s supposed to take place in the future, as there are elements of magic and fantasy mixed in with the sci-fi. Overall it’s strongly recommended if you’re a sci-fi/fantasy fan.

Parable of the Sower and Parable of the Talents by Octavia Butler (1993, 1998)

Apparently a lot of people read the Earthseed books back in high school or earlier, but these ones weren’t on my radar until this year. The first book in particular I deeply enjoyed: its vision of the future is disturbing, but frankly it’s one of the more believable sci-fi books I’ve read. It’s less about whiz-bang excursions to Alpha Centauri and more about the daily struggle of life on Earth in a warming climate.

It’s also impressive that this series was written back in the 90s, at a time when climate change wasn’t being taken as seriously as today. Nowadays it feels downright prescient – especially when you get to the so-unbelievable-I-had-to-check-the-publish-date depiction of a populist demagogue being elected on a familiar slogan. Overall I found the first book stronger than the second, but both are worth reading.

The Southern Reach Trilogy (Annihilation, Authority, Acceptance) by Jeff VanderMeer (2014)

An interesting and somewhat maddening set of sci-fi books. Unfortunately I feel that, like a lot of mysteries, the first book writes checks that the later ones can’t quite cash. You’ll probably get the most enjoyment out of it if you read the first book and ignore the rest entirely.

Just let all the mysteries from the first book sit in your mind as a delicious enigma. The second and third books don’t do a great job of clearing things up anyway.

Nonfiction

Working in Public: The Making and Maintenance of Open Source Software by Nadia Eghbal (2020)

I’m a bit biased toward this book, since I’m actually quoted in it a couple of times, but I absolutely loved this book. More than just a recapitulation of what I already know after working on open-source software for years, this book actually illuminated some things about modern open-source culture, and even some of my own motivations for writing OSS, that hadn’t really been clear to me before.

In particular, the way she draws a parallel between OSS developers and social media “content creators” was especially eye-opening for me. When you stop treating GitHub issues and pull requests as “contributions,” and start thinking of them more like comments on a YouTube video, the social dynamics start to make a lot more sense. Probably one of the best books on software I’ve ever read, up there with Don’t Make Me Think and The Design of Everyday Things in my personal pantheon.

It Doesn’t Have to Be Crazy at Work by Jason Fried and David Heinemeier Hansson (2018)

This book confirmed a lot of what I already believed, but it’s still nice to see it put to paper in a succinct way. Basecamp seems like a genuinely nice place to work, and a good example for other companies to follow.

If anything, it seems to me that software should be the opposite of an industry where people are encouraged to work 12-hour days, answer emails at all hours, and work on the weekend. The whole point of the job is to automate things so that the systems mostly run themselves. If you get into a purely reactive mode, then it can be a kind of death-spiral where you’re constantly inserting humans into the critical paths of the overall system, which makes everything more fragile and doesn’t play to the strengths of computing in general.

Twilight of Democracy: The Seductive Lure of Authoritarianism by Anne Applebaum (2020)

Over the past few years, I’ve been kind of obsessed with the question of why we’re experiencing a worldwide shift towards illiberalism. I think previous entries in my year-end book reviews do a better job of answering that question, but Applebaum’s book is a more intimate, insider’s story of what it feels like to see this shift play out even among one’s closest friends.

I get the feeling that, among conservatives in particular, the Cold War created an odd set of alliances and bedfellows (free-marketers, foreign-policy hawks, evangelicals), that’s starting to break down. This book is worth reading if you’re interested in those kinds of larger ideological shifts.

The New Class War: Saving Democracy from the Managerial Elite by Michael Lind (2017)

I picked up this book because it was recommended alongside Ezra Klein’s similar Why We’re Polarized. I didn’t actually finish Klein’s book (probably because I picked up enough bits and pieces from his excellent podcast), but I did read this, and I find it to be maybe a more complete picture of why politics feels so fractured nowadays.

The basic argument of the book is that policy decisions in western democracies are increasingly being made by a technocratic elite, and that a backlash is underway from the broader populace that doesn’t feel represented in the new system. In the broad strokes of history, that may be a pretty familiar picture, but the book tells an interesting story of how we got there. A good pairing would be Listen, Liberal by Thomas Frank, about how the Democratic party gradually lost its working-class base.