Maybe we should limit our use of margins in CSS

May 29, 2020

Recently, I came across this short post by Max Stoiber, the author of styled-components, who argues that we should ban margins from our components. I suppose he was writing with React in mind but the idea extends to any component-based framework such as Vue or Angular.

It’s a very short post but the gist of it can be found in the following sentences:

Margin breaks component encapsulation. A well-built component should not affect anything outside itself.

Margin makes reusability harder. Good components are usable in any context or layout.

This really resonated with me. I think one of the main pain points of traditional CSS is that a small change has the potential to break many other styles in different parts of an application. So anything we can do to achieve more isolation and encapsulation is very welcome.

How exactly do margins break component encapsulation?

Imagine we’ve written a beautiful <Accordion> component that we need to display on our homepage. At this stage, the homepage may be the only place where we’re using <Accordion> and in this particular case, we need to add some space between it and say some blocks of text above and below it.

// Homepage.js
const Homepage = () => (
  <Paragraph>Some text...</Paragraph>
  <Accordion />
  <Paragraph>Some text.... </Paragraph>
);
// Accordion.js
const Accordion = () => (
  // Inline styles used here for simplicity
  <div style={{ marginTop: '40px', marginBottom: '40px' }}>
     // implementation here....
  </div>
);

Great, so we have styled the homepage correctly by adding the appropriate spacing between <Accordion> and the sorrounding <Paragraph> components.

The trouble with this approach is that the 40px vertical margins are now hardcoded inside <Accordion>. This means that if we now need to use <Accordion> elsewhere, say on a new FAQ page, we have to somehow override the hardcoded 40px margin if we have different spacing requirements.

This makes <Accordion> less reusable because it has styling hardcoded within that assumes we always want 40px vertical margins. We cannot just drop in the component on other pages – we need to override its default styling.

There are many ways we could override the margins depending on whether we’re using CSS modules or a CSS-in-JS approach like styled-components.

The CSS modules approach to overriding styles

// FaqPage.js
const FaqPage = () => (
  <Heading>Some heading...</Heading>
  <Accordion className={styles.faqAccordion} />
  <Paragraph>Some text... </Paragraph>
)
// FaqPage.css
.faqAccordion {
  // custom margin
}

The CSS-in-JS approach to overriding styles

// FaqPage.js
const FaqPage = () => (
  <Heading>Some heading...</Heading>
  <FaqAccordion />
  <Paragraph>Some text... </Paragraph>
)
// FaqPage.styles.js
export const FaqAccordion = styled(Accordion)`
  // custom margin
`;

To be fair, these approaches do not seem too bad initially, especially the CSS-in-JS approach. But still, it requires extra work which is both unnecessary and which adds noise and clutter to our code.

Replacing margins with spacer components

So what alternative do we have? I suggest we could remove the hardcoded margins from <Accordion> and instead utilize utility spacer components like so:

// FaqPage.js
const FaqPage = () => (
  <Heading>Some heading...</Heading>
  <Spacer size="xs" /> // or could be unit / numeric based
  <Accordion />
  <Spacer size="xs"> 
  <Paragraph>Some text... </Paragraph>
);

This way, all of the spacing responsibility is offloaded from our components which we should try to keep completely isolated from its context.

Now, if we want to use <Accordion> on other pages, we can just use different size props on <Spacer> components and not have to override <Accordion> itself.

Closing thoughts

I don’t think we should ban margin completely as there are cases where its usage is probably sensible such as on elements inside a top-level component.

For example, if we are creating a <PrimaryNavigation> component that will only be used in one place, then it’s probably overkill to use <Spacer> components to provide the spacing between any nested menu links as that spacing will not need to be changed.

This idea of shunning margins is meant mostly for top-level components. We should avoid adding margins to the top-most element of a component that we are likely to reuse in different layouts and contexts.