Tailwind Spacing System: Consistent Gaps, Padding and the 4px Grid
Everything you need to know about Tailwind's spacing scale, the 4px grid underneath it, and how to stop eyeballing your gaps and padding for good.
What the 4px Grid Actually Is (and Why It Matters)
The 4px grid is a constraint, not a magic trick. It means every spacing value in your UI — padding, margin, gap, width — is a multiple of 4px. That's it. Simple rule, massive impact on how coherent your layouts feel.
Tailwind's default spacing scale is built directly on this. Each step in the scale is 0.25rem, which at the browser's default of 16px font size equals exactly 4px. So space-1 is 4px, space-2 is 8px, space-4 is 16px, space-8 is 32px. The naming is the step count, not the pixel value — which trips people up constantly until it clicks.
Honestly, the reason this works so well is that 4px divides cleanly into 8px, 16px, and 32px — all the natural rhythm points the human eye expects in a layout. You'd never eyeball a 13px gap and feel good about it. On a 4px grid, that gap doesn't exist as an option, so you make better choices by default.
Worth noting: Tailwind v3 kept this same underlying math, and Tailwind v4 (released in early 2025) kept the scale but introduced CSS-native variables for every token. The grid is the same. The delivery mechanism changed.
Reading the Default Spacing Scale
Tailwind ships a scale from 0 (0px) through 96 (384px), with some half-steps mixed in at the small end. Here's the part of the scale you'll live in 90% of the time:
space-0 → 0px
space-px → 1px
space-0.5 → 2px
space-1 → 4px
space-1.5 → 6px
space-2 → 8px
space-2.5 → 10px
space-3 → 12px
space-4 → 16px
space-5 → 20px
space-6 → 24px
space-8 → 32px
space-10 → 40px
space-12 → 48px
space-16 → 64px
space-20 → 80px
space-24 → 96pxThe same numbers work across p-, m-, gap-, w-, h-, and every other spacing-related utility. That's the whole point — one scale, everywhere. When you set p-4 on a card and gap-4 on the grid that contains it, you're creating visual rhythm without thinking about it.
Look, the half-steps (0.5, 1.5, 2.5) exist for tight UI situations like form inputs, icon alignment, and badge padding. They're the 2px, 6px, and 10px slots. You'll use them less often than you think you will, but you'll be glad they're there when you need fine-grained control without reaching for arbitrary values.
One more thing — space-px is exactly 1px. It's there for borders and hairline dividers, not for spacing components. Don't use it for gaps between elements unless you want things to look like they're touching.
Padding, Margin, and Gap: Choosing the Right Tool
This is where most Tailwind beginners make a mess. Padding, margin, and gap all affect how elements breathe, but they're not interchangeable. Padding is internal space — inside the element. Margin is external space — between the element and its surroundings. Gap is the space between children in a flex or grid container. Using margin between flex children when you should be using gap is one of the most common spacing bugs in React codebases.
// Don't do this — margin on flex children is fragile
<div className="flex">
<Card className="mr-4" />
<Card className="mr-4" />
<Card />
</div>
// Do this instead — gap handles it cleanly
<div className="flex gap-4">
<Card />
<Card />
<Card />
</div>In practice, you should default to gap for any layout where you control the container. Reserve margin for flow content — paragraphs, headings, standalone elements dropped into prose. Reserve padding for adding breathable space inside a component: cards, buttons, nav items, badges.
Tailwind gives you directional variants for everything. px-4 sets left and right padding. py-2 sets top and bottom. pt-6 sets only the top. gap-x-4 and gap-y-8 let you control column and row gaps independently in a grid. That last one is criminally underused — most grids have different vertical and horizontal spacing needs.
Quick aside: space-x-{n} and space-y-{n} are a different beast. They use margin via a * + * adjacent sibling selector. They work in older layouts but break with flex-wrap in ways that gap doesn't. Prefer gap for any modern flexbox or grid work.
Extending the Scale: When You Need Custom Values
You'll hit a point where the default scale doesn't have what you need. Maybe your design system calls for a 28px gap that doesn't exist in the defaults (space-7 is 28px — it actually does exist, but let's pretend you needed 36px). In Tailwind v3, you extend the scale in tailwind.config.js. In v4, you do it with CSS variables directly in your stylesheet.
// tailwind.config.js (v3)
module.exports = {
theme: {
extend: {
spacing: {
'18': '72px', // 18 * 4px
'22': '88px', // 22 * 4px
'36': '144px', // keeps the 4px grid
},
},
},
}/* Tailwind v4 — in your main CSS file */
@theme {
--spacing-18: 72px;
--spacing-22: 88px;
--spacing-36: 144px;
}Notice both examples stay on the 4px grid. That's intentional. Every time you add a custom spacing value, ask yourself: is this a multiple of 4? If it's not, you're probably compensating for a design decision that should be reconsidered, not patched with an off-grid value.
That said, sometimes you genuinely need an arbitrary value — 1px for a border offset, or some pixel-perfect alignment inherited from a legacy design. Tailwind's arbitrary value syntax handles that: p-[13px], gap-[7px]. Use it sparingly. The moment your codebase is littered with p-[13px] it's a sign the spacing decisions weren't made at the system level. If you're building a full design system, check how Empire UI's component tokens handle this — they bake the 4px discipline into every component so you're not negotiating with your stylesheet on every card.
Responsive Spacing with Breakpoint Prefixes
The spacing system gets genuinely powerful when you stack breakpoint prefixes. Tailwind's default breakpoints are sm (640px), md (768px), lg (1024px), xl (1280px), and 2xl (1536px). Every spacing utility supports all of them.
<section className="px-4 py-8 md:px-8 md:py-12 lg:px-16 lg:py-20">
<h1 className="mb-4 md:mb-6 lg:mb-8">Your heading</h1>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3 lg:gap-8">
{/* cards */}
</div>
</section>This pattern — tighter spacing on mobile, more generous on desktop — is called spatial scaling and it's what separates layouts that feel designed from layouts that feel ported. On a 375px phone screen, 64px of horizontal padding would eat half your content width. On a 1440px monitor, 16px of padding looks like the element is lost in a void.
In practice, the most important responsive spacing decision is your page-level container padding. px-4 md:px-8 lg:px-16 is a sensible default that covers most cases. Everything inside the container — the gaps, the section padding — can scale more modestly. You don't need every spacing value to change at every breakpoint.
One more thing — Tailwind's container class pairs well with this, but it doesn't add any horizontal padding by default. You can add padding to the container globally in your config, or just apply responsive px- classes directly. I prefer the explicit approach: it's easier to override in specific contexts without fighting config defaults.
Spacing Tokens in a Real Design System
When you're building anything beyond a single project — an internal component library, an open-source kit, a design system that designers and devs share — you need to think about spacing tokens, not individual class names. A token is a named value that can be referenced across every layer of your stack: Figma variables, CSS custom properties, Tailwind's config. Same number, everywhere.
/* Design system tokens mapped to Tailwind spacing */
:root {
--space-xs: 4px; /* space-1 */
--space-sm: 8px; /* space-2 */
--space-md: 16px; /* space-4 */
--space-lg: 24px; /* space-6 */
--space-xl: 32px; /* space-8 */
--space-2xl: 48px; /* space-12 */
--space-3xl: 64px; /* space-16 */
}The naming convention matters less than the consistency. Whether you call them xs/sm/md/lg or 100/200/300/400, what counts is that there are only these choices — not 47 arbitrary values scattered across your codebase. Constraints make teams faster, not slower. That sounds counterintuitive until you've spent 40 minutes in a PR review arguing whether a section needs 20px or 24px of top padding.
What's the actual workflow? You set the tokens in Figma. The design exports those variables. You map them directly to Tailwind's spacing scale in your config. Developers pick from the token set, not from freehand pixel values. This is how Empire UI's component library handles internal spacing — components like cards, navbars, and modals all reference the same token layer so resizing works predictably. You can also explore the glassmorphism components to see how consistent spacing tokens hold up in visually complex components where the backdrop-blur effects need careful padding to breathe.
Common Spacing Mistakes (and How to Dodge Them)
The most common mistake: using margin to create layout gaps in flex/grid containers. We already covered this, but it's worth repeating because it's everywhere. Margin-based spacing breaks when children wrap, when items are dynamically added, or when you need different gaps at different breakpoints. gap-* is the right tool 95% of the time.
Second mistake: using padding on the wrong element. Adding p-8 to an outer wrapper when you actually need p-8 inside each card means your wrapper's background color bleeds through in ways you didn't expect. Think about where the background is before you decide where the padding lives.
Third: inconsistent vertical spacing between page sections. You'll see codebases where one section has py-12, the next has pt-16 pb-8, and another has mt-20. Pick a convention — I like py-16 md:py-24 as a section standard — and stick to it. Your page will feel unified instead of assembled.
// Messy — inconsistent section spacing
<section className="mt-20 pb-8">
<section className="py-12">
<section className="pt-16 mb-16">
// Clean — one shared convention
<section className="py-16 md:py-24">
<section className="py-16 md:py-24">
<section className="py-16 md:py-24">Fourth mistake: not removing spacing when you should. first:mt-0 and last:mb-0 are your friends. If you're mapping over an array to render a list of items, and each item has mb-4, the last item will have a ghost margin at the bottom that pushes your container out of alignment. The last:mb-0 variant fixes that without any JavaScript. Worth bookmarking alongside odd: and even: for alternating styles in tables and lists. And while you're tuning your visual system, the gradient generator and box shadow generator pair well with a locked-down spacing system — consistent spacing makes decorative effects land correctly.
FAQ
Each step in Tailwind's spacing scale equals 0.25rem, which is 4px at the browser's default 16px font size. So p-4 gives you 16px of padding, gap-8 gives you 32px of gap, and so on. The number in the class name is the step count, not the pixel count.
Use gap-* for spacing between children inside a flex or grid container — it's predictable, breakpoint-friendly, and doesn't leave orphan margins on the last child. Use margin for standalone elements in document flow — headings, paragraphs, elements outside a controlled layout container.
Yes, just extend the spacing key in tailwind.config.js (v3) or use @theme CSS variables (v4). Keep custom values as multiples of 4px to stay on the grid — so 72px, 88px, 136px are fine. Arbitrary off-grid values like 13px or 37px should be rare exceptions, not habits.
The underlying 4px scale is identical in v4. The main change is that spacing tokens are now exposed as CSS custom properties (--spacing-4: 1rem) rather than only living in the config file. This means you can reference them in plain CSS too, which is useful when styling third-party components or writing @apply blocks.