Margins and Composability in CSS

If you work with CSS you may be familiar with the Lego analogy by Nicole Sullivan.

Follow this advice and, with time, you will end up with a collection of reusable components that you can compose to build complex UIs.

This concept is so simple that even my dad got it.

Reusability and composabilty are the key to success.

There are many articles and talks around the reusability topic, but I have rarely read about composability in CSS so I figured I’d write something about it.

Composability

Composability is a system design principle that deals with the inter-relationships of components.

A highly composable system provides recombinant components that can be selected and assembled in various combinations to satisfy specific user requirements.

Composability is about making the pieces play nicely together.

CSS Rules that affect composability compare to Lego studs: they let us assemble components in various combinations to satisfy specific user requirements.

Unlike Lego bricks though, UI components are not just glued together with studs. They are laid out and spaced in different ways to adapt to the context of the current view.

In this game margins play a big role.

Margins

Composability in CSS is ruled by margin among other things.

Margins are often global and set to arbitrary standard values at the beginning of a project.

Take this code for example:

/** typography.css */
h1 { margin: 3em 0; }
p { margin-bottom: 1em; }

/** components/form.css */
.form { margin-bottom: 3em; }
.form__input { margin-bottom: 1em; }

Here we have some global typography and default margin-bottom for the form component. A similar setup can be found in many popular or in-house CSS frameworks.

The code is well organized in separate files and it is reusable, but depending on the use-case it could be hard to compose the components without having to reset a couple of rules.

There are some things to keep in mind when building UI components or a pattern library:

  • Context and default margins
  • Margin direction

Context and default margins

When defining base styles or building a new component it is hard to know in advance where UI components are going to be used.

The effect of globals is unpredictable — this is a gotcha in JavaScript world, yet many CSS developers don’t want to accept the fact that this is true for styles too.

In the context of a header for example a default margin-bottom on the form may have undesired effects and has to be reset.

I have seen and had to reset similar rules many times — design choices were made years ago by someone who didn’t or couldn’t predict my case of use, and now I have to deal with it :)

UI components instead should be self-contained. The component root should be free of any rule that may affect composability, specifically:

  • margins
  • layout rules like position (absolute or fixed), float, transform, etc
  • width

We can then use higher-order components or utilities to fit the component into a specific context.

Margin direction

The margin direction of child elements can also affect composability.

In 2012 Harry Roberts wrote about Single-direction margin declarations.

The basic premise is that you should try and define all your margins in one direction.

By choosing a single direction the effect of margins is more predictable and there are fewer side effects.

However child elements can still affect the surrounding components:

take an unordered list whose items have a margin-bottom, any adjacent component/element or parent container will be affected by the margin-bottom of the last item.

My suggestion is to always reset peripheral margins and to not set margins on the component root at all.

.list { margin: 0; }
.list__item { margin-bottom: 2em; }
.list__item:last-child { margin-bottom: 0; }

An improvement is to always use margin-top and margin-left and reset the :first-child instead.

This is no different from resetting the :last-child except for the fact that the :first-child pseudo-class is supported by legacy browsers as well.

Finally we can avoid to reset rules by using the adjacent sibling selector +.

.list__item + .list__item { margin-top: 2em; }