No Outer margin
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” margin
s and padding
s?
I define “outer” here as margin
s 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 margin
s and padding
s 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 margin
s 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 margin
s 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 Item
s next to each other. Let’s give them different background colors so it’s clear which one is which:
Now that I have many Item
s, 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 Item
s, 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 Item
s 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 H2
s 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
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 Item
s:
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 aborder
orbackground-color
to visually define theborder-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.