A Tale of Two Buttons
Much ink has been spilled about different approaches to styling UI elements on web pages and apps. The prevailing wisdom is for designers and developers to approach UI design and development in a modular way. While I wholeheartedly agree with this mindset (and have been advocating it for a long time), I fear that it is sometimes mistakenly used as an argument for avoiding one of CSS’s most powerful features: The cascade.
Modular UI code
In terms of ease of development and maintainability it makes a lot of sense to group the code that a UI component is comprised of. Typically that is the code that defines what DOM elements the component is rendered as, the code that determines how those elements are visually styled and the code that deals with any interactive behaviour (such as responding to clicks or changes in content). Often there will also be some accompanying test code and documentation.
Modularising code into re-usable, easy to maintain, decoupled chunks like this is second nature to software developers. They don’t only do it for UI code — they do it for everything. It’s why programming languages have evolved to give us things like functions, objects, modules, libraries and so on.
Against that backdrop, it’s easy to see why CSS’s global and cascading nature may feel alien and counter-intuitive to many software developers. Unless you say otherwise, a lot of your component’s styling attributes are inherited from its parent elements. For instance, if you don’t explicitly say what colour your headings should appear in, they will take on whatever text colour their parent has. Those parents in turn may also be inheriting the colour and this continues all the way up, until you hit the root <html>
element.
From the perspective of an individual component, taken out of context, that may seem scary. You don’t control or know what colour (or font, or size, or whatever) it will appear in. How do you then ensure it matches the design? How do you test it?
Consequently people have created ever more elaborate means of isolating themselves from the dreaded cascade. I frequently read people claiming that CSS’s global and cascading nature doesn’t scale or that it somehow doesn’t work for complex applications.
Respectfully, I disagree. I am therefore writing this article to make the case for the cascade.
A typical scenario
To make my case, I’m going to work through a simple but also quite common example: Our good old friend and canonical example UI component, the button.
To make things a little more interesting, lets imagine there are 2 variants of our button:
- A normal one, for when it sits on the page’s default, light background colour
- An inverted one, for when it sits on sections with a dark background (perhaps in the page headers and footers)
Other things to note about our design:
- The font size and family of the buttons is the same as for body copy
- The light yellow (
cornsilk
) colour is the body’s default background colour - The dark red (
darkred
) colour is the default text colour for body copy
In case you were wondering, this colour combination passes WCAG 2.0 AAA guidelines, so we can rest assured that these colour choices are not impeding accessibility.
BEM-style classes
Let’s have a go at styling this with CSS class following the popular BEM convention. We might then organise our button CSS as follows:
- A block class
button
for the default (dark) button style. - A modifier class
button--inverted
for the alternate (light) style when the button sits on a dark background. - For each, we also need to add some styles for the pressed (aka “active”) state of the button using the
:active
pseudo-class selector
You can see an implementation of this in the following Codepen. In order to show the buttons in their intended context, they each been placed in <div>
containers with container
and container--inverted
classes for the default and inverted backgrounds respectively.
Everything looks as intended. All good, right?
There are however some improvements we can make. As it stands, we are relying on developers remembering to correctly apply the button--inverted
modifier class whenever they place a button into a container with the darker background. Or, if the page design changes and we need to change the background of an area containing buttons, then we have the additional burden of changing all their class names.
If, instead of writing static HTML, you were using a JS framework like React to construct your DOM, then you’d most likely have a prop on your button component for switching to the inverted style. Something like:
<MyButton><MyButton inverted="true">
However, the same core issue remains: You are depending on developers who use your component (and/or CSS) code to know when to apply the inverted style.
Contextual class
What would be nice, is if our button simply adopted the desired styling automatically, based on where it is located on the page. We’d then no longer need the button--inverted
modifier class (or React component prop, or other equivalent) and our button component would become foolproof.
Luckily, that’s easily done with a simple CSS descendent selector:
.button {
/* Apply default button styles here */
}.container--inverted .button {
/* Apply inverted button styles here */
}
We are literally saying to the browser “when the button is inside a container that has the inverted styles, apply these styles to it”. You can see it in action, along with the full code, in the following Codepen. Note how only the selector has changed in the CSS, and the class button--inverted
has been removed from the HTML.
Another benefit of this approach is that, often, it will align better with designer’s mental model. It’s unlikely they’d have been thinking “I want to make X colour variants of my buttons”. Far more likely they would have been thinking “Sometimes buttons need to be placed onto the darker background. As it stand’s they’ll blend in with their surroundings and just look like plain text. I’d better make an alternate design for that scenario”.
Understanding the intent behind a UI design is always helpful for UI development. If, as a developer you can appreciate why things have been styled or laid out in a particular way, then you can often express the same intent in code and produce a better implementation as a result.
As I have written before, a good UI implementation must incorporate far more than simply how things look in the designer’s mock-ups. Achieving that requires close collaboration between designers and developers. Trying to understand each others’ train of thought goes a long way towards that end.
All that being said, I’m not advocating excessively long chains of descendent selectors in your CSS. Left unchecked, that sort of thing can quickly make your CSS code bloated and convoluted. However, intentionally encoding design intent as we are in this example, I believe is a perfectly acceptable use of descendent selectors. Like everything else in life: Use them in moderation!
Exploiting inheritance
Great! We now have a chameleon button that will change its colours based on where it is on the page. That’s good for ease of use (for other developers) and maintenance.
However, if we take a closer look at our CSS, there seems to be a fair bit of redundancy. The two colours get mentioned many times. We’ve also had to explicitly set our button’s font-size
to match that of the page.
Let’s use CSS’s cascading powers to reduce our code! For starters, rather than setting an explicit font size for our button, we can simply inherit the font size from the parent element, using the inherit
keyword:
.button {
font-size: inherit;
}
(Note that most HTML elements inherit their font-size by default, so you don’t need to write any CSS to get this behaviour. Buttons and inputs are amongst the exceptions, so that is why we need to explicitly tell them to inherit this property)
Now we have less redundancy in our code. The font size is only set on the body
, so if we need to change it we only change it there and the button automatically follows suit. An added bonus is that our button’s font size will now always match its parent’s. Let’s imagine we place our button into an area of the page with with smaller font size, that button’s text will shrink to match!
We can use the same technique with the colours. While we can’t get around setting explicit colour value for the button’s default state, since border, background and text all differ from the parent, we can avoid it for the :active
state. Let’s take a closer look at their current CSS code:
/* The existing active state styles */.button:active {
color: darkred; /* This is the same as the body text color! */
background-color: cornsilk; /* Same as the body bg! */
}.container--inverted .button:active {
color: cornsilk; /* Same as inverted container's text color! */
background-color: darkred; /* Same as inverted bg! */
}
Since all those colours match the values of their parent elements, we can replace the explicit values with inherit
:
/* Use the cascade, Luke! */.button:active {
color: inherit;
background-color: inherit;
}.container--inverted .button:active {
color: inherit;
background-color: inherit;
}
Hmm… now our :active
styles are exactly the same, regardless of whether this button sits in an inverted container or not. Let’s use that to our advantage and remove the now redundant .container--inverted .button:active
block:
/* Yipee! Less code */.button:active {
color: inherit;
background-color: inherit;
}
That’s the CSS taken care of. Let’s now take another look at our HTML. Using the button
class on a button
element in our HTML looks a bit silly, especially if we’re also using type="button"
for any non-submit buttons:
<!-- Yo dawg, I heard you like buttons... --><button class="button" type="button">
If we replace our .button
class selectors in the CSS with button
element selectors, we can avoid having to use this redundant class in our HTML. Don’t be afraid of using element selectors in your CSS! It can be a great way of encouraging the correct, semantic elements being used in the corresponding markup.
You can see the result of putting it all together in this Codepen:
Enhancing with custom properties
At this point, we’ve got some succinct CSS code that will style <button>
elements anywhere on the page in an intelligent, context-aware way. To do so, we’ve only used basic CSS features that have enjoyed broad browser support for a long time. Wonderful!
But, can we do better? Well, if we’re happy for only browsers that support CSS custom properties (aka “CSS variables”) — which is most current browsers except Internet Explorer — to render the target design, then yes. We should of course aim to have a simpler fall back design for older browsers that we progressively enhance with the custom properties. However, for brevity, I’ll skip that in this example.
We’re going to use a similar approach to the one Simurai wrote about in his post “Contextual styling with custom properties”. Unlike, SASS or LESS variables, CSS custom properties can have their values overridden within the CSS cascade. Looking at our design, we only use 2 colours throughout: Our standard foreground colour, darkred
, and our standard background colour, cornsilk
. Let’s define some custom properties for them:
:root {
--fg-color: darkred;
--bg-color: cornsilk;
}
We then update our body
styles to reference them rather than the explicit colour values:
body {
color: var(--fg-color);
background-color: var(--bg-color);
}
Our inverted container class is where it starts to get interesting. Here, we override the values of our custom properties, thereby also changing them for any child elements that reference those custom properties. Additionally, we use them to set the container’s colour and background colour as needed:
.container--inverted {
--fg-color: cornsilk;
--bg-color: darkred;
color: var(--fg-color);
background-color: var(--bg-color);
}
With all that set up, let’s return our attention to the button again. As it stands, we still have to set some default button colours and then override those values for buttons that reside within an inverted container:
button {
border-color: darkred;
color: cornsilk;
background-color: darkred;
}.container--inverted button {
border-color: cornsilk;
color: darkred;
background-color: cornsilk;
}
Let’s swap these explicit colour values out for the corresponding custom properties:
button {
border-color: var(--fg-color);
color: var(--bg-color);
background-color: var(--fg-color);
}.container--inverted button {
border-color: var(--fg-color);
color: var(--bg-color);
background-color: var(--fg-color);
}
Note how, for the button within an inverted container, --fg-color
will resolve to cornsilk
(and--bg-color
to darkred
), since we overrode those values in the .container--inverted
class.
Would you look at that? Now the CSS blocks for both button variants is identical! Nixing the inverted block that we no longer need leaves us with:
button {
border-color: var(--fg-color);
color: var(--bg-color);
background-color: var(--fg-color);
}
The result of putting everything together is now visible in this Codepen:
As you can see, it is 100% visually identical to our original BEM-style solution. However, we now have:
- Buttons that adapt their colours and font-size to those of their parent elements
- Cleaner, simpler markup
- More succinct CSS code that will be quicker for our users to download
Furthermore, if we wanted to have more container styles for different themes, it’s trivially easy to add them and our button component will continue to blend in without us needing to modify any of its CSS. If that’s not “scalable”, I don’t know what is.
Here’s a little example of this in action:
Conclusion
The designs and examples used in this article are of course simple ones. Nonetheless, they have hopefully succeeded in demonstrating how deliberate use of CSS’s cascade can simplify and reduce the code that UI developers need to produce. You absolutely can extend this style of architecting and writing your CSS code to all kinds of UI components besides buttons.
Furthermore, and perhaps more importantly, I think embracing CSS’s cascade can be a great way to encourage consistency and simplicity in UIs. Rather than every new component being a free for all, it trains both designers and developers to think in terms of aligning with and re-using what they already have.
Remember, every time you set a property in CSS you are in fact overriding something (even if it’s just the default user agent styles). In other words, CSS code is mostly expressing exceptions to a default design. The more you do so, the more your UI components become sealed-off blocks of UI that don’t necessarily blend into or adapt to their surroundings.
Consequently, my approach tends to be to see how little CSS code I can get away with. I’ll start with just the markup I need and see how that renders. If something doesn’t look the way I need it to, I’ll first try to understand why. What computed values are my properties inheriting and from where? Should I override them, or is there parent element further up the inheritance chain that should be amended in some way? It’s just like the old adage says: “Measure twice, cut once”.
Once in a while I want to inherit things that, by default or due to styles inherited from some other part of my CSS code, aren’t. That’s where little gems like inherit
, initial
and unset
can work wonders.
If you’ve not approached your CSS in this way before, I highly recommend giving it a go. Hopefully you’ll be pleasantly surprised by how much redundant code you can remove and by the increased flexibility and adaptability it can afford your individual UI components.