In my experience, improper spacing management is one of the most common ways a design system becomes misused.
In order to preserve modularity of UI components in a design system, you should properly separate spacing concerns in your layout.
When you are driving your car at a high speed on a highway, you need to maintain greater distance between your car and the car in front of you than if you are driving in a city. To ensure optimal space between them, imagine that each car on the highway had a long rod protruding from the front of it.
Now imagine if you dropped these cars with their rods in a busy city setting. The traffic would grind to a halt.
const Car = () => (
<div style={{
width: 50,
height: 100,
backgroundColor: 'red',
marginTop: 200 // the “rod” of our car
}} />
)
const Highway = ({children}) => (
<div style={{display: 'flex', flexDirection: 'column'}}>
{children}
</div>
)
const City = ({children}) => (
<div style={{display: 'flex', flexDirection: 'column'}}>
{children}
</div>
)
const World = () => (
<Traffic>
{/* All is good ✅ */}
<Highway>
<Car />
<Car />
<Car />
</Highway>
{/* Chaos! Panic! Mayhem! 💥 */}
<City>
<Car />
<Car />
<Car />
</City>
</Traffic>
)
It is the same with components (if you disregard all the ways cars and UI components differ).
If you have a button or a card component with built-in margins, you are likely going to run into a scenario where these margins are a hindrance to your layout.
Component should not blindly assume spacing in different contexts.
The trade-off of modularity is a little bit of extra work (and possibly, code).
So, what options do we have?
Solutions
As a rule of thumb, the parent which contains the component should worry about spacing between its children and not the child itself.
Use “Stack” component
The sole purpose of this component is to maintain proper spacing between its children. It is a very useful tool and easy to build if you are creating components from scratch.
<Stack spacing={20}>
<Button/>
<Button/>
<Button/>
<Button/>
</Stack>
Or, if you want to stick to the (this time, rodless) car analogy:
<Stack spacing={200}> // Highway
<Car/>
<Car/>
<Car/>
</Stack>
<Stack spacing={15}> // City
<Car/>
<Car/>
<Car/>
</Stack>
Here is how MUI, Ant and Bootstrap implement these components.
Pros
- Keeps the code clean
- It’s sole purpose is to be concerned with spacing
Cons
- Not very flexible if you need to have different spacing between children
Use wrapper component
This works best when you have a single component that needs spacing.
The purpose of the wrapper is just to position the nested component in the layout.
<Layout>
<SomeComponent/>
<Box marginRight={20}>
<Component/>
</Box>
<Component/>
<Box marginLeft={40}>
<Component/>
</Box>
</Layout>
Pros
- Easy to use in any context
Cons
- Clutters up the code a bit
Provide the component with optional spacing
Provide your component with a spacing prop that can be used to maintain spacing between components.
const Component = ({spacing}) => (
<div style={{marginRight: spacing || false}}/>
)
const App = () => (
<Layout>
<Component spacing={20}/>
</Layout>
)
Pros
- Keeps the code clean
- Intuitive in a way that it follows the traditional HTML api
Cons
- You need to implement spacing prop in every component to keep the API consistent
- Kind of feels like going against the idea of separation of concerns because on the same principle, you can apply any other styling property to the component
- Adds a bit of clutter to the component itself
Conclusion
There is no one-size-fits-all solution. Choose a solution depending on the use case. Sometimes, the best course of action is to use a combination of these solutions.
The rule of thumb is, let the parent component worry about spacing between its children.
Important note
Spacing concerns do not apply to the padding inside the component itself. Padding should provide permanent spacing between the content and the edge of the component.