February 26, 2024

No Outer margin

edit

Building reusable components can be challenging to do well, and there are some anti-patterns that are easy to fall into that can make it even more difficult.

One of the most common antipatterns I see is having margin (and in some cases padding) on the outermost element of a component. This is an easy mistake to make, but luckily, it’s easily avoided once we know what we’re looking for.

What are “outer” margins and paddings?

I define “outer” here as margins that extend beyond the border-box of the UI. Generally speaking, this would be applying margin to the outermost element of a component.

function Card({ children }) {
  return (
    // WARNING: Don't do this! It is an outer `margin`!
    <div style={{ marginBottom: '1rem' }}>{children}</div>
  )
}

function EmployeeCard({ name, occupation }) {
  return (
    <Card>
      {/**
       * This is fine! We can use internal margins for layout,
       * but there are better ways we'll learn later!
       */}
      <div style={{ marginBottom: '1rem' }}>{name}</div>
      <div>{occupation}</div>
    </Card>
  )
}

In addition to never using outer margin, we should never use padding for the same purpose. The outermost element of a component should not have padding unless that element has a border or a background-color.

function Card({ children }) {
  return (
    // WARNING! Don't do this! It is an outer `padding`!
    // Without a border or background color, it has
    // the same appearance/effect as a `margin`
    <div style={{ paddingBottom: '1rem' }}>{children}</div>
  )
}

function BorderCard({ children }) {
  return (
    <div
      style={{
        border: '1px solid tomato',
        backgroundColor: 'gray',
        // This padding is ok because we have styles that extend to the
        // border-box of our component, thus defining a boundary for
        // this UI!
        padding: '1rem',
      }}
    >
      {children}
    </div>
  )
}

What’s the big deal? Why is this a problem/anti-pattern?

The problem with outer margins and paddings like the examples above is that it breaks encapsulation. I’ve written about encapsulation before, but never in terms of UI. Essentially, styles like these have an impact on the other components around it. Components with outer margins get their elbows out, NBA Jam style, knocking around adjacent components.

The impact of these styles often lead to what I call “compensations”, essentially workarounds to undo what we’ve already done. We’ll see some examples of this soon.

Not only do outer margins require compensations, they give the underlying component too many responsibilities. External spacing should never be the responsibility of a component. Components should only ever be concerned with internal spacing.

An abstract example

Let’s say I have a reusable component called Item that looks like this:

function Item() {
  return <div className="rounded bg-accent p-8" />
}

Imagine Item is any reusable UI. Perhaps it’s an InfoCard or a ListRow in a list. It doesn’t really matter what the specifics of Item are, we only need to know that it’s a single unit of UI for our purposes.

Let’s say I have many Items next to each other. Let’s give them different background colors so it’s clear which one is which:

Now that I have many Items, it’s necessary to space them away from each other. It might be very tempting to add a margin-bottom to each one, like so:

function Item() {
  // Notice the `mb-4` class
  return <div className="mb-4 rounded bg-accent p-8" />
}

That looks fine, but we need to look at it with more scrutiny. What if we add a border around the wrapping element? What are we going to see?

Oof! We have an extra margin at the bottom.

It’s at this point less experienced devs start making all sorts of “compensations” to handle this situation. If it’s a dynamically rendered list of Items, we might be tempted to use an index in our loop and somehow remove the margin-bottom on the last one. We might even go so far as to add the dreaded marginBottom={false} prop, a bad case of Yet Another Prop syndrome if I’ve ever seen one.

// Don't do this. You'll make me cry.
function Item({ marginBottom = true }) {
  const mb = marginBottom ? 'mb-4' : ''
  return <div className={`rounded bg-accent p-8 ${mb}`} />
}

But what about other scenarios? What are you going to do if you have to put your Items into a Row:

Or a Grid?

Are you going to make boolean props for all the margins? I sure hope not!

A concrete example

One of the most common ways I see this is with headings. Let’s say I have a component for H2s in my codebase:

function H2({ children }) {
  return <h2 className="mb-4 text-xl font-bold">{children}</h2>
}

That looks like this:

Using the h2. Notice the space below.

This might seem like a good idea. Headings tend to be found in large blocks of text and doesn’t the user agent stylesheet give h1 through h6 a margin-bottom by default anyways?

Sure, but the web isn’t simply long-form text anymore. It’s componentized and we need to be able to use our elements in more scenarios than just long-form text.

What if we want to add additional data around it? Perhaps we need to put a subtitle right below it, or a date, or timestamp?

Using the h2

Uh oh. I’m spaced really far away!

Now you’re back-pedaling to either undo the mb-4 class or compensate for it some other way. Better to leave it off entirely and do the proper solution, which we’ll discuss next.

The solution

The solution to this problem is simple, use parent elements or components for layout and spacing.

As I said before, it is never the responsibility of a component to manage its external spacing, only its internal spacing. We can always use a parent element or component to manage the layout of adjacent components.

Regardless of what layout solution you use, whether it’s with components with something like Chakra UI or classes like Tailwind, I find the best solutions heavily use Flex or Grid containers with gap to apply spacing. Let’s do that here with our Items:

After removing the mb-4 class from our Item, we can now wrap our items in another element to do the spacing. For example, our Stack looks like this:

Our Row looks like this:

And our Grid looks like this:

No matter where we need to put our Item, we know we can control how it’s laid out in our UI. We’ve fixed its encapsulation problem and made it more reusable.

Summary

  • No margin on the outermost element
  • No padding on the outermost element unless it has a border or background-color to visually define the border-box boundary of the UI
  • Use parent elements or components to do layout instead

Other articles

You can’t write an article about this topic without giving credit and reference to Max Stoiber’s “Margin considered harmful”. Give it a read.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Want to read more?
Newer Post: Agathist Q&A
Older Post: Type TODO
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Logo for Array.reduce()
Array.reduce()
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.