Create your design system, part 4: Spacing
In this article we’ll take a look at how to set a spacing system in CSS, and how to take advantage of relative units to handle responsiveness.
This article is part of a series on design systems inspired by our (upcoming) library of web components. The library relies on a solid system of CSS globals. So this is us sharing the things we’ve learned setting the global style of our library! ✌
A library of customizable HTML, CSS, JS web components for designers and developers.codyhouse.co
Setting the Spacing system using CSS Variables
Step one of setting a spacing system is creating a scale of (spacing) values. To create a scale of non-linear values you need 1) a unit or base value and 2) a multiplier. We’ve already talked about creating a modular scale in our article on web typography.
A basic approach to spacing would look like this:
The snippet above describes a spacing system based on the Fibonacci sequence (each value is found by adding up the two numbers before it).
Note: The first time I read about applying the Fibonacci sequence to a scale of spacing values was in this excellent article by The Scenery.
The downside of a basic approach is that, well…it’s basic. If we want to change a single spacing value, we have to do the math and update all other values manually.
Let’s try to bring some control over the system. We can introduce a variable to define the unit number, and use the calc() function to obtain the spacing values.
We’re getting closer! In the example above, we’ve defined each spacing value by multiplying the unit number by a multiplier. Note that the Fibonacci sequence is still applied to the multipliers. Now if we want to update the whole system, we can edit the unit value.
Next, let’s get rid of the absolute unit and replace it with the em unit. To do so, we can replace 16px with 1em:
What looks like a small tweak, actually changes a lot! The
em unit is a relative unit equal to the current font size. In most browsers, the default font-size (before CSS styling is applied) is 16 pixels. Therefore we can assume 1em = 16px. However, if you edit the font-size of an element, 1em is no longer 16px (for that element), but it’s equal to the new font-size. What appears like a lack of control, is a powerful responsiveness shortcut. Let me show you why.
If you’ve been following this series on design systems, you know we‘ve created a typography system where all font sizes are intertwined and obtained by multiplying a text-base-size variable (equal to 1em) by a ratio. That means that the text-base-size variable is the controller of the whole type system. If you increase the text-base-size at a specific media query, all the text size variables change accordingly.
By updating just one variable, you get this:
Not just that! Since the spacing unit is equal to 1em, and all other spacing values are multipliers of the unit value, when we update the text-base-size variable, we affect the spacing as well. 💪
Look how this method affects typography and spacing at the same time:
We’re still updating a single variable (text-base-size). No additional media queries needed so far! All you have to do to take advantage of this powerful approach to responsiveness is using the spacing variables to set paddings and margins on a component level:
What if you want to update all spacing values at once, without having to change the text-base-size variable? Just update the space-unit variable:
It is true that by embracing a method like this one you lose some of your “visual” control, but it’s in favor of simplicity and maintainability. It is true that you won’t be able to set pixel-perfect values, but you will deal with responsiveness to a whole new level: most of the work will be setting a robust scale of values for spacing and typography. Then you can edit 2–3 variables at specific media queries to create a cascade effect, as opposed to tweaking all components and sub-elements.
How to set a default padding for all components
There will be cases where you need different components to have the same padding. Setting a variable for the default padding of the components is a trick I’ve learned after a lot of time spent looking for a padding value hidden somewhere in my CSS files. 😓
To be clear, I’m referring to those blocky, “container-like” components.
Here’s an example:
Getting back to our _spacing.scss file, let’s include the component padding variable:
By doing so, you create another “dev shortcut”: if you need to apply padding to a component, you don’t need to check which spacing variable has been used in other similar cases. Just use the component-padding variable.
Margin utility classes
While it’s safe to include padding directly in your component CSS, including margins can cause layout issues. Once again, we’re referring to the main components, those blocks that define your main layout (not a <button> component, but a <section class=”myComponent”> element).
If we imagine these components as blocks distributed in layouts, then information about margins and position should be stored in the abstract layout element, not the component. For example, you may end up using the same component in two different layouts, in one case you need to apply a margin-bottom, in another you don’t. You can see why including the margin-bottom in the component CSS may not be a good idea in this case.
For these reasons, it can be handy to have utility classes for the margins:
Optionally, you can update the margin values at a specific breakpoint, to automatically increase distances when the viewport is bigger.
Custom spacing values
What if you’re setting a margin on an element, and space-sm looks too small, while space-md is too big? Working with a set of predefined variables means that sometimes you have to compromise (for the sake of the system maintainability).
BUT! If you do need to use a spacing value that is not included in your spacing scale, and you don’t want to edit or break the system, you can take advantage of the CSS calc function:
By doing so, you’re introducing a new spacing value, but you’re still making sure this new value is “controlled” by the same system.
Fixed spacing values
The spacing system we’ve introduced implies that all margins, paddings (and typography elements) are updated simultaneously using a couple of variables. As powerful as this can be, there are cases when you don’t want a spacing value to change, regardless of the viewport size.
To do so, you need to define spacing using a different unit:
rem is relative to the html (root) font-size. Therefore it’s not affected when you edit the body font size (updating the text-base-size variable).
To be clear, I’m not suggesting to replace
rem, but to combine them.
The easiest way to set a “fixed” spacing value would be:
However, that would mean introducing a bunch of custom, out-of-the-system, spacing values. If we think about a real case scenario: a new developer joins your team, they open a CSS file and notice you’ve set a 0.5rem margin on an element. In an ideal dev world, you’ve informed your new colleague of the spacing system, or the project has (up-to-date) CSS guidelines, or the colleague asks for more info on how spacing is handled in the project. In the real dev world, chances are the new teammate will assume spacing is set using custom
To avoid confusion, it would be ideal to use variables. If we do so, the colleague is forced to check the _spacing.scss file, where they’ll find (100%! 😇) your helpful comments.
Here’s an example of how you can (optionally) set a scale of “fixed” spacing values in addition to the responsive ones:
Putting it all together, here’s our _spacing.scss file:
Here’s where we are with the spacing system so far! If you have feedback/suggestions, let us know in the comment!
I hope you enjoyed the article! For more web design nuggets, follow us here on Medium or Twitter. 🙌