When Margin Isn't the Right Fit
Consider a simple HomeButton component for a site header:
import { ArrowUpLeft } from 'lucide-react';
function HomeButton() {
return (
<a href="/">
<ArrowUpLeft size={24} />
Go back home
</a>
);
}
export default HomeButton;
The icon sits too close to the label. The common fix is to wrap the text in a <span> and apply a margin:
import { ArrowUpLeft } from 'lucide-react';
function HomeButton() {
return (
<a href="/">
<ArrowUpLeft size={24} />
{/* 👇 The fix 👇 */}
<span style={{ marginLeft: 16 }}>
Go back home
</span>
</a>
);
}
export default HomeButton;
But margin brings its own baggage—collapsing behavior, unclear ownership of the space, and leakage across component boundaries. There’s a more explicit alternative: create a dedicated element whose only job is to create space.
import { ArrowUpLeft } from 'lucide-react';
import Spacer from './Spacer';
function HomeButton() {
return (
<a href="/">
<ArrowUpLeft size={24} />
<Spacer size={16} />
Go back home
</a>
);
}
export default HomeButton;
That idea is not new. It’s a direct descendant of the spacer GIF, a technique that largely disappeared but deserves a second look in modern component-driven architectures.
Remembering the Spacer GIF
In the late 90s, opening a site’s source often revealed this:
<img alt="" src="spacer.gif" width="1" height="32" />
Back then, layouts were built with HTML tables. Empty table cells would collapse, breaking the design, so developers inserted a 1×1 transparent GIF to keep cells open. Since GIF was the only format supporting transparency at the time, this tiny invisible image became a universal tool for creating buffers between elements. It could be stretched to any size to produce an invisible gap.
The spacer GIF’s demise came with the rise of CSS. Early CSS required float hacks to replace tables, but it eventually won the debate over separation of concerns. HTML was for structure, CSS for presentation. Presentational tags like <font> and <center> were purged. The spacer GIF was collateral damage.
Then React shifted the paradigm again. HTML is now generated from JavaScript, and tools like styled-components blend all three concerns back together. Component-driven development has made us comfortable mixing structure and presentation once more—which is exactly why the spacer concept fits again.
This isn’t about dismissing separation of concerns. It’s about recognizing that no single convention is universally right. What matters is having clear boundaries; whether they run along technology lines or component lines is secondary.
A Modern Spacer Component
Here’s the implementation:
// Spacer.js
import styled from 'styled-components';
function getHeight({ axis, size }) {
return axis === 'horizontal' ? 1 : size;
}
function getWidth({ axis, size }) {
return axis === 'vertical' ? 1 : size;
}
const Spacer = styled.span`
display: block;
width: ${getWidth}px;
min-width: ${getWidth}px;
height: ${getHeight}px;
min-height: ${getHeight}px;
`;
export default Spacer;
If you’re not using a CSS-in-JS library, here’s the plain React version:
// Spacer.js
import React from 'react';
const Spacer = ({
size,
axis,
style = {},
...delegated,
}) => {
const width = axis === 'vertical' ? 1 : size;
const height = axis === 'horizontal' ? 1 : size;
return (
<span
style={{
display: 'block',
width,
minWidth: width,
height,
minHeight: height,
...style,
}}
{...delegated}
/>
);
};
export default Spacer;
The only required prop is size, which defaults to a square:
// Produces a 16px × 16px gap:
<Spacer size={16} />
You can also specify a single axis:
// Produces a 32px × 1px gap:
<Spacer axis="horizontal" size={32} />
The component uses pixel values because optical alignment often calls for non-standard measurements. The pattern adapts readily to design tokens:
<Spacer space="sm" />
<Spacer space="md" />
<Spacer space="lg" />
<Spacer space="xl" />
Why a Span, Not a Div
An earlier version rendered a div, but that’s limiting. The HTML spec forbids placing divs inside elements like p and button. A span is more flexible, but it defaults to display: inline, which doesn’t accept explicit width or height. Setting display: block solves that for separating block-level elements. When you need to separate inline content, compose it differently:
const InlineSpacer = styled(Spacer)`
display: inline-block;
`;
Resisting Compression
In addition to width and height, the component sets min-width and min-height. Width is more of a suggestion than a hard rule, as this example with a constrained parent container shows:
import { ArrowUpLeft } from 'lucide-react';
function HomeButton() {
return (
<a href="/" style={{ maxWidth: 150 }}>
<ArrowUpLeft size={24} />
{/* Quick Spacer implementation */}
<span
style={{
display: 'inline-block',
width: 16,
height: 16,
background: 'hotpink',
}}
/>
Go back home
</a>
);
}
export default HomeButton;
The pink box represents the spacer. With insufficient room, the browser squeezes the empty child rather than the content around it. min-width is a firmer constraint, telling the browser this element shouldn’t be compressed. That matters because consistent spacing is part of a polished interface—a button’s icon gap shouldn’t change just because the label starts wrapping.
Going Responsive
The basic <Spacer /> is static. When you need viewport-dependent spacing, extend the API:
<Spacer
size={32}
when={{
lgAndUp: 64,
xlAndUp: 96,
}}
/>
The prop name when reads naturally from the consumer’s perspective. The implementation depends on your styling solution and theme tokens.
Why Not Just Use Margin?
There are several concrete reasons to prefer an explicit spacer component:
- Neither the icon nor the text should "own" the space between them; it’s a separate layout concern.
- Margins collapse in surprising ways, adding mental overhead that never fully disappears.
- Adding a wrapping
<span>around text for a margin inserts an extra layer that can break grid and flex layouts. - Margins bleed across component boundaries, which is fundamentally at odds with encapsulated components.
Several developers are already moving away from margin in favor of layout components. The <Spacer> is a useful tool in that shift. On sites where it has been adopted widely, it’s added about a hundred instances over a couple of years without complaints. DOM size is a consideration—Google suggests staying under 1500 nodes—but in practice, most pages need only a handful of spacers. Most spacing still comes from padding, gap, and other layout primitives, with margin retained for occasional use.
An Old Idea Resurfacing
Intriguingly, spacer GIFs are making an unexpected comeback as a service at spacerGIF.org, which has seen its traffic grow substantially over the past year. For developers who remember the messy table-based web, the concept carries heavy baggage. But component architectures give this old trick a legitimate new purpose. It’s worth a second look before writing it off.



