Snippet

Margin

edit

Update: This component was formerly named Spacer. It was brought to my attention that a few design systems use Spacer in the way of old spacer.gifs, arbitrarily sized empty boxes used for pushing elements around. Thus, I’ve renamed this component to Margin. It’s more accurate anyways.


It is a mistake for reusable components to export their own margins. Doing so makes it difficult to compose them with other components to build up more complex ones. For more on this concept, I encourage you to read Max Stoiber’s article, “Margin considered harmful”.

When we compose components, we can use other elements to affect their layout. This can be done with one-off elements with styles applied, or we can create some declarative layout components. One of the ones I find most useful is the Margin component you see below. I have used the component you see below in a number of applications. I use a Margin all over this blog, but I make a small modification which I will show later on.

I even added some jsdoc comments and some TypeScript types for you.

type TMargin = number | string

type MarginProps = {
  children: React.ReactNode
  /**
   * Shorthand for applying margin to all sides
   */
  all?: TMargin
  /**
   * Shorthand for applying margin to left and right sides
   */
  horz?: TMargin
  /**
   * Shorthand for applying margin to top and bottom sides
   */
  vert?: TMargin
  /**
   * Applies margin to the top
   */
  top?: TMargin
  /**
   * Applies margin to the right
   */
  right?: TMargin
  /**
   * Applies margin to the bottom
   */
  bottom?: TMargin
  /**
   * Applies margin to the left
   */
  left?: TMargin
}

function Margin({
  children,
  all = 0,
  horz = 0,
  vert = 0,
  top = 0,
  right = 0,
  bottom = 0,
  left = 0,
}: MarginProps) {
  const margins = {
    ...(all && {
      marginTop: all,
      marginRight: all,
      marginBottom: all,
      marginLeft: all,
    }),
    ...(horz && {
      marginLeft: horz,
      marginRight: horz,
    }),
    ...(vert && {
      marginTop: vert,
      marginBottom: vert,
    }),
    ...(top && { marginTop: top }),
    ...(right && { marginRight: right }),
    ...(bottom && { marginBottom: bottom }),
    ...(left && { marginLeft: left }),
  }

  return <div style={margins}>{children}</div>
}

Now we can compose components together. A simple example might be a Name and Avatar component that may have different layouts in different contexts. Perhaps in one layout they are adjacent to one another, as inline or flex elements:

<div style={{ display: 'flex' }}>
  <Margin right={8}>
    <Avatar src={avatar} />
  </Margin>
  <Name>{name}</Name>
</div>

And in another, perhaps a Bio component is introduced and some of them are stacked:

<div>
  <Margin bottom={8}>
    <Name>{name}</Name>
  </Margin>
  <div style={{ display: 'flex' }}>
    <Margin right={4}>
      <Avatar src={avatar} />
    </Margin>
    <Bio>{bio}</Bio>
  </div>
</div>

The Margin component makes it very easy to arbitrarily create new layouts for reusable components. I think you’ll enjoy learning to use it.

My modification

Because I use shevyjs in my projects, I modify my Margin to use the baseSpacing function under the hood. Here is a direct copy/paste of what I’m using in this blog, right now:

import React from 'react'
import { bs } from '../shevy' // alias of the baseSpacing function I export

export default function Margin({
  children,
  all = 0,
  vert = 0,
  horz = 0,
  top = 0,
  right = 0,
  bottom = 0,
  left = 0,
}) {
  const margins = {
    ...(all && { margin: bs(all) }),
    ...(vert && { marginTop: bs(vert), marginBottom: bs(vert) }),
    ...(horz && { marginLeft: bs(horz), marginRight: bs(horz) }),
    ...(top && { marginTop: bs(top) }),
    ...(right && { marginRight: bs(right) }),
    ...(bottom && { marginBottom: bs(bottom) }),
    ...(left && { marginLeft: bs(left) }),
  }

  return <div css={margins}>{children}</div>
}

Now, when I pass a number in, it’s being passed to bs and always returns a value that’s in rhythm with my vertical spacing.

You might also notice that I use the margin shorthand for the all prop. Since I’m the only one working on kyleshevlin.com, I’m not worried about making a mistake combining props on this component. You may want to bake in stronger safety rails on your version of the component, disallowing overriding the all prop with other props. I leave that up to you.


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.

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 Introduction to State Machines and XState
Introduction to State Machines and XState
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.