Back

Spacing children in React

11min • 19 September 2021

I was really impressed when Mark Dalgleish(new tab) first introduced me to the space prop they built into Braid Design System(new tab).
In this post, I want to guide you through the journey of spacing children. From pure CSS solutions, to generic components in React. We'll look at all the potential issues and how to built them most flexible and accessible spacing implementation.

Spacing children with CSS

Adding equal space between children is such a common task and yet it wasn't as straight-forward as it could be.

I know, we have CSS Grids and grid-gap nowadays, but grid is not always the right solution to it.

Before React and CSS in JS libraries became a thing, we used to add spacing with CSS selectors, e.g.:

Note: Don't get me wrong here, there's nothing wrong with pure CSS solutions relying on selectors. Heydon Pickering(new tab) introduced the lobotomized owl axiomatic CSS(new tab) methodology years ago as a low effort CSS-only solution to component spacing. That being said, there're good reasons why people might prefer newer solutions.

.parent .child {
margin-bottom: 10px;
}

Looking at how this renders, we might notice an issue. There's a 10px space after the last child as well. To fix that, we have to explicity unset the margin/padding for either the first or last child, depending on the property we choose. So our CSS rather looks something like this:

.parent .child {
margin-bottom: 10px;
}
.parent .child:last-child  {
margin-bottom: 0;
}

Fair enough, with modern CSS selectors, we could trim this down to just one selector again:

.parent .child:not(:last-child)  {
margin-bottom: 10px;
}

But this is still not as simple as I want it to be.

Spacer component for everything

With React's release, we ventured into a new frontend era: component-based design.
Instead of repeating our HTML and CSS over and over, we're now able to write components once and reuse them everything.

This sourced all kinds of new ways to handle spacing within a component.
One very common candidate is the Spacer component. It allows us to add spacing in a very declarative and easy-to-understand way:

<Parent>
<Child />
<Spacer size={10} />
<Child />
<Spacer size={10} />
<Child />
</Parent>

The main issue with that, is that as soon as the number of children grows it becomes rather repetitive and messy.
Additionally, most of the time, we don't actually render our children manually, but map over an array to render them.

Doing that, we run into an old problem again: We have to handle the last child.

<Parent>
{children.map((child, index, arr) => (
<>
{child}
{index < arr.length - 1 && <Spacer size={10} />}
</>
))}
</Parent>

Another drawback of the Spacer component is the extra amount of empty HTML elements rendered all over the place, but let's ignore that for now. But, the Spacer component still has its staying in existence! Whenever we need single, individual spacers, the Spacer component comes in really handy!

The space prop

What if, all we have to do is passing a space prop to our parent and everything is handled automatically?

<Parent space={10}>{children}</Parent>

It doesn't matter how many children we render, there's always a 10px space in between them.

At first, this might sound really easy to implement, right?
We basically already solved it in two different ways above, so we can just reuse that logic within our parent component.

Generic component

Instead of applying to logic to this specific parent-child pair, we can have a generic component. It's usually called Box or View in many design systems and component libraries.

I will use a imaginary css-prop to simplify the implementation for demo reasons. Many CSS in JS librarires actually provide such a css prop or function anyways.

function Box({ space, children }) {
return (
<div
css={{
'& > *:not(:last-child)': {
marginBottom: space,
},
}}>
{children}
</div>
)
}

This can now be reused everywhere!

<Box space={10}>
<Child />
<Child />
<Child />
</Box>

Horizontal layouts

Now you might ask, what about horizontal layouts?
Our current Box implementation only works on vertical layouts.
We could go ahead and rename that to Vbox and add another Hbox where V and H stand for vertical and horizontal respectively.

The Vbox would use marginBottom where the Hbox would use marginRight instead.

<Vbox space={10}>
// we can even nest them
<Hbox space={40}>
<Child />
<Child />
<Child />
</HBox>
<Child />
<Child />
</Vbox>

Flexbox

We can even be more clever and leverage the flexbox layout system.
It provides the flex-direction(new tab) property which decides if items are rendered horizontally or vertically.

If we make our Box component a flexbox container, we can check for the direction passed and adapt the margin property accordingly.

function Box({ space, direction = 'row', children }) {
const spaceProperty = direction === 'row' ? 'marginRight' : 'marginBottom'
return (
<div
css={{
display: 'flex',
flexDirection: direction,
[`& > *:not(:last-child)`]: {
[spaceProperty]: space,
},
}}>
{children}
</div>
)
}

Now we have one component for both vertical and horizontal spacing!

<Box direction="column" space={10}>
// we can even nest them
<Box space={40}>
<Child />
<Child />
<Child />
</Box>
<Child />
<Child />
</Box>

Extra: If we want to support both row-reverse and column-reverse as well, we'd have to switch marginRight to marginLeft and marginBottom to marginTop as well as using the :first-child selector instead of :last-child.

Overwriting margins

Our component is already pretty flexible and universally usable, but there's one issue left.
Since we set a hard margin using the child selector, we run into issues when trying to render a child that has margins itself.
Instead of fighting a specificity war, we can instead add another wrapping element to prevent that:

function Box({ space, direction = 'row', children }) {
const spaceProperty = direction === 'row' ? 'marginRight' : 'marginBottom'
return (
<div
css={{
display: 'flex',
flexDirection: direction,
}}>
{React.Children.toArray(children).map((child) => (
<div
css={{
':not(:last-child)': {
[spaceProperty]: space,
},
}}>
{child}
</div>
))}
</div>
)
}

Now our children can have additional margins and still have the correct spacing applied in between.

<Box direction="column" space={10}>
<Child marginBottom={5} />
<Child marginBottom={20} />
<Child marginBottom={10} />
</Box>

Container element

While this fixes one issue, it creates another one.
Now that we have a wrapping container around each child, we all of a sudden can't control which element is rendered.

Imagine we want to render a list, thus using ul and li.
Right now, all of our children are wrapped in a div which would be a huge accessibility concern as screen readers would probably fail to get that right.

But React already has a neat solution for that.
Instead of rendering a hard-coded element, we can instead use a passed prop to define the component.

Most generic Box implementations already accept an as prop.

function Box({
as: Component = 'div',
space,
spaceElement: Container = 'div',
direction = 'row',
children,
}) {
const spaceProperty = direction === 'row' ? 'marginRight' : 'marginBottom'
return (
<Component
css={{
display: 'flex',
flexDirection: direction,
}}>
{React.Children.toArray(children).map((child) => (
<Container
css={{
':not(:last-child)': {
[spaceProperty]: space,
},
}}>
{child}
</Container>
))}
</Component>
)
}

And there we go!

<Box as="ul" direction="column" space={10} spaceElement="li">
<Child />
<Child />
<Child />
</Box>
  • Child 1
  • Child 2
  • Child 3

The Future

A bright future is ahead. You might remember the grid-gap property which was mentioned earlier? It has been renamed to just gap and not only works with CSS Grids but also with Flexbox layouts!

Just recently, with Safari 14.0, the final missing browser implemented the gap property for flexbox as well. That means, spacing children could be as easy as the following:

<div css={{ display: 'flex', gap: 10 }}>
<Child />
<Child />
<Child />
</div>

Summary

To quickly sum it up: We went from plain CSS spacing to a reusable React component that incorporates all of those techniques, but in a simple convenient way. The result is a fully flexible and yet highly accessible space prop. With that in place, spacing children has never been easier in React.
Even better, we'll be able to use the gap property soon and finally have a native, idiomatic solution built into CSS directly!

Credits

Thanks to Mark Dalgleish(new tab) for showing me their implementation at ReactiveConf 2019(new tab). Thanks to my friends Daniel(new tab), Timon(new tab) and Kitty(new tab) for reviewing this article!

Thanks for reading!

I’m Robin, Freelancer & Frontend Architect from Germany.
Any question regarding this article? Reach out to me on Twitter(new tab)!
You can also find me on GitHub(new tab).