This article assumes you've read the Fluid Typography Sizing and Scales first.
Spacing is the cement that holds together the bricks of components on a UI. The importance of good spacing is hard to overstate: it's the most important aspect of design. And the difference between an exquisitely elegant design and a graceless one. So it's no wonder designers are rightly taught to pay great attention to whitespace.
However, it remains one of the most unnecessarily unsystematized aspects of web design and development. Most designers still space things out by hand: Shift + Left, Shift + Right, Shift + Up, and Shift + Down to move things around by increments of 10px and counting 10, 20, 30 in their heads is the bread and butter of spacing.
As for developers, spacing is handled with hard-coded utility classes 🥴, infinitely growing BEM classes 🤢, or, God forbid, plain old ah hoc CSS 🤮. Out of all of these, utility classes is the least worse: if there's a clear spacing scale agreed upon between developer and designer, it's relatively easy to implement a given design. But if the designer decides to change a spacing rule that runs across the entire ui, you're going to have a hard time identifying all the instances that pertain to that spacing rule and update them to something else.
Why is that? Because, even if you're using utility classes, those spacing rules have been hard-coded in the UI's components.
We can do better.
Let's give spacing the same well-thought-out treatment that we gave typography with a hand-picked scale and fluid space sizing. This enables clear communication between designer and developer, preserves the designer's complete control over spacing, even after the implementation is complete, and greatly speeds up development.
For the same reason we're not going to use a modular scale for typography, we're not going to use a modular scale for spacing. The reason is that modular scales are too limited and you could try different ratios and equations, but at that point you’re just trying to pick a scale that happens to match the sizes you already know you want.
It's more pragmatic to hand-pick the values because this approach grants the designer complete control over the number and variety of space sizes.
Let's take a look at TailwindCSS's spacing scale.
Name | Size | Pixels | Visual representation |
---|---|---|---|
0 | 0px | 0px | |
px | 1px | 1px | |
0.5 | 0.125rem | 2px | |
1 | 0.25rem | 4px | |
1.5 | 0.375rem | 6px | |
2 | 0.5rem | 8px | |
2.5 | 0.625rem | 10px | |
3 | 0.75rem | 12px | |
3.5 | 0.875rem | 14px | |
4 | 1rem | 16px | |
5 | 1.25rem | 20px | |
6 | 1.5rem | 24px | |
7 | 1.75rem | 28px | |
8 | 2rem | 32px | |
9 | 2.25rem | 36px | |
10 | 2.5rem | 40px | |
11 | 2.75rem | 44px | |
12 | 3rem | 48px | |
14 | 3.5rem | 56px | |
16 | 4rem | 64px | |
20 | 5rem | 80px | |
24 | 6rem | 96px | |
28 | 7rem | 112px | |
32 | 8rem | 128px | |
36 | 9rem | 144px | |
40 | 10rem | 160px | |
44 | 11rem | 176px | |
48 | 12rem | 192px | |
52 | 13rem | 208px | |
56 | 14rem | 224px | |
60 | 15rem | 240px | |
64 | 16rem | 256px | |
72 | 18rem | 288px | |
80 | 20rem | 320px | |
96 | 24rem | 384px |
This is a comprehensive enough scale, however a few notes.
First, there's one major no-go: semantics are tied with implementation. The names of the steps in the scale (1, 1.5, 2, 2.5, etc) are tied to multiples of 4px, which is an implementation detail. In order for the designer to have complete control over the design after the implementation is done, semantics and implementation have to be decoupled. This is absolutely crucial for development speed.
The other awkward thing is that we have half steps (0.5, 1.5, 2.5, etc) on the lower end of the scale because we need multiples of 2px, and we skip steps (64, 72, 80, etc) on the higher end of the scale because we don't need those multiples of 4px. This inconsistency makes for a harder-to-communicate scale and an awkward developer experience where if the designer asks you to step a component's spacing up, you don't go from 64 to 65, but from 64 to 72 🤦♂️.
Let's do better.
Name | Size | Pixels | Visual representation |
---|---|---|---|
0 | 0px | 0px | |
1 | 1px | 1px | |
2 | 0.125rem | 2px | |
3 | 0.25rem | 4px | |
4 | 0.375rem | 6px | |
5 | 0.5rem | 8px | |
6 | 0.625rem | 10px | |
7 | 0.75rem | 12px | |
8 | 0.875rem | 14px | |
9 | 1rem | 16px | |
10 | 1.25rem | 20px | |
11 | 1.5rem | 24px | |
12 | 1.75rem | 28px | |
13 | 2rem | 32px | |
14 | 2.25rem | 36px | |
15 | 2.5rem | 40px | |
16 | 2.75rem | 44px | |
17 | 3rem | 48px | |
18 | 3.5rem | 56px | |
19 | 4rem | 64px | |
20 | 5rem | 80px | |
21 | 6rem | 96px | |
22 | 7rem | 112px | |
23 | 8rem | 128px | |
24 | 9rem | 144px | |
25 | 10rem | 160px | |
26 | 11rem | 176px | |
27 | 12rem | 192px | |
28 | 13rem | 208px | |
29 | 14rem | 224px | |
30 | 15rem | 240px | |
31 | 16rem | 256px | |
32 | 18rem | 288px | |
33 | 20rem | 320px | |
34 | 24rem | 384px |
This simple change is deceptively powerful.
First, the designer doesn't have to think about absolute values (4px, 10px, 16px, etc), but relative ones ("this should be more spaced out than that by 2 steps", "that should be more spaced out than this by 4 steps", etc). Eventually the designer has to converge on absolute values, of course, but as long as the designer starts with a somewhat decent scale, the flexibility to think in relative terms is a huge speed booster, specially in the early phases of design.
Also, if the designer gets the relative values right - which is much easier than to get the absolute values right - the implementation will not change: step 30 will still be step 30, whether the designer decides later that that means 240px or 244px. This frees the developer to develop at full speed and, given an initial mockup, in parallel, without having to wait for the designer to get the values right.
Finally, by giving the designer complete control over the absolute values, we eliminate an entire category of back-and-forth work between designer and developer! The only possible mistakes or changes that require coordination between the two are those that involve a different relative step. But this is a much smaller set of changes and it would most likely involve changing a single class on a single component, for example.
As is the case with typography, we're not going to use breakpoint-based sizing because it creates a jarring user experience and an unsustainable burden on designers and developers.
As is the case with typography, if we're going to change the sizing at different viewport widths, we don't need as many steps, because a space of 384px would always be too overwhelming (it would overflow, in fact), on a 320px wide viewport.
And we need no variation in the lower steps, some from step 15 onwards, and more variation in the higher steps because of the two opposing forces between the need to increase the spacing on larger screens to keep the design balanced and the fact that users don't resize their browser windows to see the same content bigger, but to see more content - and therefore we don't want to just scale up the entire UI (that'd be a terrible UX).
So let's cap the scale at step 20 and add a min and a max space size.
Name | Pixels (min) | Pixels (max) | Visual representation |
---|---|---|---|
0 | 0px | 0px | |
1 | 1px | 1px | |
2 | 2px | 2px | |
3 | 4px | 4px | |
4 | 6px | 6px | |
5 | 8px | 8px | |
6 | 10px | 10px | |
7 | 12px | 12px | |
8 | 14px | 14px | |
9 | 16px | 16px | |
10 | 20px | 20px | |
11 | 24px | 24px | |
12 | 28px | 28px | |
13 | 32px | 32px | |
14 | 36px | 36px | |
15 | 40px | 48px | |
16 | 48px | 64px | |
17 | 64px | 96px | |
18 | 80px | 144px | |
19 | 128px | 256px | |
20 | 160px | 416px |
Resize your browser window to see how the bars change size starting from step 15 onwards.
The technique we're using here is generally referred to, in software engineering, as "inversion of control". We create an abstraction (the scale) and implement our UI using it. This allows the designer to retain complete control over the specific values each step in the scale maps to.
In conclusion, as is the case with typography, between modular or hand-picked scales, hand-picked scales offer the right amount of control, while between breakpoint-based and fluid space sizing, fluid space sizing offers a significantly more efficient design and development process. This allows the developer to start working right after mockups are available, while giving the designer complete control over spacing, and offers a smoother (let's call it "fluid" 😉) user experience.
The knowledge I'm sharing in these articles is encoded in my andrecasal/ui component library. All you need from the designer is a mockup and potentially some layout rules (i.e. what to do as the viewport width increases), and you can hit the ground running building your app. Refine the design details later!
Golden nuggets of in-depth code knowledge. Delivered to your inbox every 2 weeks.
Once you subscribe you'll get my free guide to modern full-stack web development and solve analysis paralysis from choosing which tools to use.