Managing Flow and Rhythm with CSS Custom Properties
An important part of designing user interfaces is creating consistent vertical rhythm between elements. Creating consistent, predictable space doesn’t just make your web pages and views look better, but it can also improve the scan-ability.
Browsers ship with default CSS and these styles often create consistent rhythm for flow elements out of the box. The problem is though that we often reset these styles with a reset. Elements such as <div>
and <section>
also have no default margin or padding associated with them.
I’ve tried all sorts of weird and wonderful techniques to find a balance between using inherited CSS while also levelling the playing field for component driven front-ends with very little success. This experimentation is how I landed on the flow utility, though and I’m going to show you how it works. Let’s dive in!
The Flow utility
With the ever-growing number of folks working with component libraries and design systems, we could benefit from a utility that creates space for us, only when it’s appropriate to do so. The problem with my previous attempts at fixing this is that the spacing values were very rigid.
That’s fine for 90% of contexts, but sometimes, it’s handy to be able to tweak the values based on the exact context of your component. This is where CSS Custom Properties come in handy.
The code
.flow {
--flow-space: 1em;
}
.flow > * + * {
margin-top: var(--flow-space);
}
What this code does is enable you to add a class of flow
to an element which will then add margin-top
to sibling elements within that element. We use the lobotomised owl selector to select these siblings. This approach enables an almost anonymous and automatic system which is ideal for component library based front-ends where components probably don’t have any idea what surrounds them.
The other important part of this utility is the usage of the --flow-space
custom property. We define it in the .flow
component and each element within it will be spaced by --flow-space
, by default. The beauty about setting this as a custom property is that custom properties also participate in the cascade, so we can utilise specificity to change it if we need it. Pretty cool, right? Let’s look at some examples.
A basic example
What we’ve got in this example is some basic HTML content that has a class of flow
on the parent article element. Because there’s a very heavy-handed reset added as a dependency, all of the content would have been squished together without the flow
utility.
Because our --flow-space
custom property is set to 1em
, the space between elements is 1X the font size of the element in question. This means that a <h2>
in this context has a calculated margin-top
value of 28.8px
, because it has an assigned font size of 1.8rem
. If we were to globally change the --flow-space
value to 1.1em
for example, we’d affect everything because margin values would be calculated as 1.1X the font size.
This example looks great because using font size as the basis of rhythm works really well. What if we wanted to to tweak certain elements within this article, though?
I like lots of whitespace with my article layouts, so the 1em
space isn’t going to cut it for all elements. I like to provide plenty of space between headed sections, so I increase the --flow-space
in these instances:
h2 {
--flow-space: 3rem;
}
Notice also how I also switch over to using rem
units? I want to make sure that these overrides are always based on the root font size. This is a personal preference of mine and you can use whatever units you want. Just be aware that it’s better for accessibility to use flexible units like em
, rem
and %
, so that a user’s font size preferences are honoured.
A more advanced example
Although the flow
utility is super useful for a plethora of contexts, it really shines when working with a few unrelated components. Instead of having to write specific layout CSS just for your particular context, you can use flow
and --flow-space
to create predictable and contextual space.
In this example, we’ve got ourselves a little prototype layout that features a media element, followed by a grid of features. By using flow
, it was really quick and easy to generate space between those two main elements. It was also easy to create space within the components. For example, I added it to the .media__content
element, so that the article’s content would space itself:
<article class="media__content flow">
...
</article>
Something to remember though: the custom properties cascade in the same way that other CSS values do, so you’ve got to keep that in mind. We’ve got a great example of that in this example where because we’ve got the flow
utility on our .features
component, which has a --flow-space
override: the child elements of .features
will inherit that value, so we’ve had to set another value on the .features__list
element.
“But what about old browsers?”, I hear you cry
We’re using CSS Custom Properties that at the time of writing, have about 88% support. One thing we can do to remedy the other 12% of browsers is to set a default, traditional margin-top value of 1em
, so it calculates itself based on the element’s font-size:
.flow {
--flow-space: 1em;
}
.flow > * + * {
margin-top: 1em;
margin-top: var(--flow-space);
}
Thanks to the cascading and declarative nature of CSS, we can set that default margin-top value and then immediately set it to use the custom property instead. Browsers that understand Custom Properties will automatically apply them—those that don’t will ignore them. Yay for the cascade and progressive enhancement!
Wrapping up
This tiny little utility can bring great power for when you want to consistently space elements, vertically. It also—thanks to the power of the modern web—allows us to create contextual overrides without creating modifier classes or shame CSS.
If you’ve got other methods of doing this sort of work, please let me know on Twitter. I’d love to see what you’re working on!
About the author
Andy Bell is an independent designer and front-end developer who’s trying to make everyone’s experience on the web better with a focus on progressive enhancement and accessibility.