Random spacing destroys visual coherence. A button gets 12px padding. A card gets 20px. A section gets 35px. Each component picks arbitrary values, creating chaotic layouts that feel disconnected.
The problem isn't spacing itself—it's the lack of systematic spacing architecture.
Why Spacing Needs a System
Spacing isn't just whitespace. It creates:
- Visual rhythm - Consistent spacing intervals create flow
- Hierarchy - Larger spacing groups related content
- Breathing room - Proper spacing improves readability
- Alignment - Systematic spacing enables predictable layouts
- Scalability - Spacing tokens adapt to different screen sizes
Without a system, spacing becomes guesswork.
Building a Base-Unit Spacing Scale
Start with a mathematical scale based on a fundamental unit:
// tokens/spacing.ts
// Base unit (typically 4px or 8px)
const baseUnit = 4;
// Generate spacing scale
export const spacing = {
0: '0',
px: '1px',
0.5: `${baseUnit * 0.5}px`, // 2px
1: `${baseUnit * 1}px`, // 4px
2: `${baseUnit * 2}px`, // 8px
3: `${baseUnit * 3}px`, // 12px
4: `${baseUnit * 4}px`, // 16px
5: `${baseUnit * 5}px`, // 20px
6: `${baseUnit * 6}px`, // 24px
8: `${baseUnit * 8}px`, // 32px
10: `${baseUnit * 10}px`, // 40px
12: `${baseUnit * 12}px`, // 48px
16: `${baseUnit * 16}px`, // 64px
20: `${baseUnit * 20}px`, // 80px
24: `${baseUnit * 24}px`, // 96px
32: `${baseUnit * 32}px`, // 128px
40: `${baseUnit * 40}px`, // 160px
48: `${baseUnit * 48}px`, // 192px
56: `${baseUnit * 56}px`, // 224px
64: `${baseUnit * 64}px`, // 256px
} as const;
// Semantic spacing tokens
export const spacingSemantic = {
// Component padding
padding: {
xs: spacing[2], // 8px
sm: spacing[3], // 12px
md: spacing[4], // 16px
lg: spacing[6], // 24px
xl: spacing[8], // 32px
},
// Layout gaps
gap: {
xs: spacing[2], // 8px
sm: spacing[4], // 16px
md: spacing[6], // 24px
lg: spacing[8], // 32px
xl: spacing[12], // 48px
},
// Section spacing
section: {
sm: spacing[12], // 48px
md: spacing[16], // 64px
lg: spacing[24], // 96px
xl: spacing[32], // 128px
},
// Container padding
container: {
sm: spacing[4], // 16px
md: spacing[6], // 24px
lg: spacing[8], // 32px
xl: spacing[12], // 48px
},
} as const;
This creates a predictable scale where every value is a multiple of 4px.
The 8-Point Grid System
Many design systems use an 8px base unit for easier mental math:
// tokens/spacing-8pt.ts
const baseUnit = 8;
export const spacing8pt = {
0: '0',
1: '8px', // 1 unit
2: '16px', // 2 units
3: '24px', // 3 units
4: '32px', // 4 units
5: '40px', // 5 units
6: '48px', // 6 units
8: '64px', // 8 units
10: '80px', // 10 units
12: '96px', // 12 units
} as const;
Choose based on your design needs. 4px offers more granularity; 8px simplifies decisions.
Component-Level Spacing Tokens
Define spacing for specific component types:
// tokens/component-spacing.ts
import { spacing } from './spacing';
export const componentSpacing = {
button: {
paddingX: spacing[4], // 16px
paddingY: spacing[2], // 8px
gap: spacing[2], // icon-to-text gap
},
input: {
paddingX: spacing[3], // 12px
paddingY: spacing[2], // 8px
gap: spacing[3], // label-to-input gap
},
card: {
padding: spacing[6], // 24px
gap: spacing[4], // internal content gap
headerGap: spacing[3], // title-to-description gap
},
form: {
fieldGap: spacing[4], // between form fields
sectionGap: spacing[8], // between form sections
labelGap: spacing[2], // label-to-input
},
list: {
itemGap: spacing[3], // between list items
contentGap: spacing[2], // within list item content
},
navigation: {
itemGap: spacing[1], // compact nav items
sectionGap: spacing[6], // between nav sections
},
} as const;
Building Spacing-Aware Components
Create components that consume spacing tokens automatically:
// components/Card.tsx
import { forwardRef, HTMLAttributes } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
const cardVariants = cva(
[
'rounded-lg',
'border',
'border-neutral-200',
'bg-white',
].join(' '),
{
variants: {
padding: {
none: 'p-0',
sm: 'p-4', // 16px
md: 'p-6', // 24px
lg: 'p-8', // 32px
xl: 'p-12', // 48px
},
gap: {
none: 'space-y-0',
sm: 'space-y-2', // 8px
md: 'space-y-4', // 16px
lg: 'space-y-6', // 24px
xl: 'space-y-8', // 32px
},
},
defaultVariants: {
padding: 'md',
gap: 'md',
},
}
);
interface CardProps
extends HTMLAttributes<HTMLDivElement>,
VariantProps<typeof cardVariants> {}
export const Card = forwardRef<HTMLDivElement, CardProps>(
({ padding, gap, className, children, ...props }, ref) => {
return (
<div
ref={ref}
className={cardVariants({ padding, gap, className })}
{...props}
>
{children}
</div>
);
}
);
Card.displayName = 'Card';
// Compound components for semantic structure
export const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={`space-y-2 ${className || ''}`} {...props} />
)
);
CardHeader.displayName = 'CardHeader';
export const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
className={`text-xl font-semibold ${className || ''}`}
{...props}
/>
)
);
CardTitle.displayName = 'CardTitle';
export const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p
ref={ref}
className={`text-sm text-neutral-600 ${className || ''}`}
{...props}
/>
)
);
CardDescription.displayName = 'CardDescription';
export const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={className} {...props} />
)
);
CardContent.displayName = 'CardContent';
export const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={`flex items-center gap-2 ${className || ''}`}
{...props}
/>
)
);
CardFooter.displayName = 'CardFooter';
Usage:
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter
} from '@/components/Card';
import { Button } from '@/components/Button';
export function ProductCard() {
return (
<Card padding="md" gap="md">
<CardHeader>
<CardTitle>Premium Plan</CardTitle>
<CardDescription>
Best for growing teams
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">$29<span className="text-base font-normal">/mo</span></p>
<ul className="mt-4 space-y-2">
<li>✓ Unlimited projects</li>
<li>✓ Advanced analytics</li>
<li>✓ Priority support</li>
</ul>
</CardContent>
<CardFooter>
<Button className="w-full">Get Started</Button>
</CardFooter>
</Card>
);
}
Spacing is consistent without manual className tweaks.
Stack Component for Vertical Rhythm
Build a layout primitive for vertical spacing:
// components/Stack.tsx
import { forwardRef, HTMLAttributes } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
const stackVariants = cva(
'flex flex-col',
{
variants: {
gap: {
0: 'gap-0',
1: 'gap-1', // 4px
2: 'gap-2', // 8px
3: 'gap-3', // 12px
4: 'gap-4', // 16px
6: 'gap-6', // 24px
8: 'gap-8', // 32px
12: 'gap-12', // 48px
16: 'gap-16', // 64px
},
align: {
start: 'items-start',
center: 'items-center',
end: 'items-end',
stretch: 'items-stretch',
},
},
defaultVariants: {
gap: 4,
align: 'stretch',
},
}
);
interface StackProps
extends HTMLAttributes<HTMLDivElement>,
VariantProps<typeof stackVariants> {}
export const Stack = forwardRef<HTMLDivElement, StackProps>(
({ gap, align, className, ...props }, ref) => {
return (
<div
ref={ref}
className={stackVariants({ gap, align, className })}
{...props}
/>
);
}
);
Stack.displayName = 'Stack';
Usage:
export function SettingsPanel() {
return (
<Stack gap={8}>
<section>
<h2 className="text-2xl font-bold mb-4">Account Settings</h2>
<Stack gap={4}>
<Input label="Email" type="email" />
<Input label="Username" type="text" />
<Input label="Bio" as="textarea" rows={4} />
</Stack>
</section>
<section>
<h2 className="text-2xl font-bold mb-4">Notifications</h2>
<Stack gap={3}>
<Checkbox label="Email notifications" />
<Checkbox label="Push notifications" />
<Checkbox label="SMS notifications" />
</Stack>
</section>
</Stack>
);
}
Inline Component for Horizontal Spacing
Create a horizontal spacing primitive:
// components/Inline.tsx
import { forwardRef, HTMLAttributes } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
const inlineVariants = cva(
'flex flex-row',
{
variants: {
gap: {
0: 'gap-0',
1: 'gap-1',
2: 'gap-2',
3: 'gap-3',
4: 'gap-4',
6: 'gap-6',
8: 'gap-8',
},
align: {
start: 'items-start',
center: 'items-center',
end: 'items-end',
baseline: 'items-baseline',
},
justify: {
start: 'justify-start',
center: 'justify-center',
end: 'justify-end',
between: 'justify-between',
around: 'justify-around',
},
wrap: {
true: 'flex-wrap',
false: 'flex-nowrap',
},
},
defaultVariants: {
gap: 2,
align: 'center',
justify: 'start',
wrap: false,
},
}
);
interface InlineProps
extends HTMLAttributes<HTMLDivElement>,
VariantProps<typeof inlineVariants> {}
export const Inline = forwardRef<HTMLDivElement, InlineProps>(
({ gap, align, justify, wrap, className, ...props }, ref) => {
return (
<div
ref={ref}
className={inlineVariants({ gap, align, justify, wrap, className })}
{...props}
/>
);
}
);
Inline.displayName = 'Inline';
Usage:
export function ActionBar() {
return (
<Inline gap={3} justify="between" align="center">
<h1 className="text-2xl font-bold">Dashboard</h1>
<Inline gap={2}>
<Button variant="secondary">Export</Button>
<Button variant="primary">Create New</Button>
</Inline>
</Inline>
);
}
Responsive Spacing
Define breakpoint-specific spacing:
// tokens/responsive-spacing.ts
export const responsiveSpacing = {
container: {
paddingX: {
sm: spacing[4], // 16px mobile
md: spacing[6], // 24px tablet
lg: spacing[8], // 32px desktop
xl: spacing[12], // 48px wide
},
},
section: {
paddingY: {
sm: spacing[12], // 48px mobile
md: spacing[16], // 64px tablet
lg: spacing[24], // 96px desktop
},
},
grid: {
gap: {
sm: spacing[4], // 16px mobile
md: spacing[6], // 24px tablet
lg: spacing[8], // 32px desktop
},
},
} as const;
Implement with responsive utilities:
// components/Container.tsx
import { forwardRef, HTMLAttributes } from 'react';
export const Container = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
return (
<div
ref={ref}
className={`
w-full
max-w-7xl
mx-auto
px-4
sm:px-6
lg:px-8
xl:px-12
${className || ''}
`}
{...props}
/>
);
}
);
Container.displayName = 'Container';
Grid Systems with Token-Based Gaps
Build flexible grid layouts:
// components/Grid.tsx
import { forwardRef, HTMLAttributes } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
const gridVariants = cva(
'grid',
{
variants: {
cols: {
1: 'grid-cols-1',
2: 'grid-cols-1 md:grid-cols-2',
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
6: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-6',
},
gap: {
0: 'gap-0',
2: 'gap-2',
4: 'gap-4',
6: 'gap-6',
8: 'gap-8',
12: 'gap-12',
},
},
defaultVariants: {
cols: 3,
gap: 6,
},
}
);
interface GridProps
extends HTMLAttributes<HTMLDivElement>,
VariantProps<typeof gridVariants> {}
export const Grid = forwardRef<HTMLDivElement, GridProps>(
({ cols, gap, className, ...props }, ref) => {
return (
<div
ref={ref}
className={gridVariants({ cols, gap, className })}
{...props}
/>
);
}
);
Grid.displayName = 'Grid';
Usage:
export function ProductGrid({ products }: { products: Product[] }) {
return (
<Grid cols={3} gap={6}>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</Grid>
);
}
Section Spacing for Page Layout
Create consistent page section rhythm:
// components/Section.tsx
import { forwardRef, HTMLAttributes } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
const sectionVariants = cva(
'w-full',
{
variants: {
spacing: {
none: 'py-0',
sm: 'py-12', // 48px
md: 'py-16', // 64px
lg: 'py-24', // 96px
xl: 'py-32', // 128px
},
background: {
transparent: 'bg-transparent',
white: 'bg-white',
neutral: 'bg-neutral-50',
accent: 'bg-accent-50',
},
},
defaultVariants: {
spacing: 'md',
background: 'transparent',
},
}
);
interface SectionProps
extends HTMLAttributes<HTMLElement>,
VariantProps<typeof sectionVariants> {}
export const Section = forwardRef<HTMLElement, SectionProps>(
({ spacing, background, className, ...props }, ref) => {
return (
<section
ref={ref}
className={sectionVariants({ spacing, background, className })}
{...props}
/>
);
}
);
Section.displayName = 'Section';
Build full page layouts:
export function LandingPage() {
return (
<>
<Section spacing="xl" background="accent">
<Container>
<Stack gap={6} align="center">
<h1 className="text-5xl font-bold text-center">
Build Better Products
</h1>
<p className="text-xl text-center max-w-2xl">
Ship consistent, accessible UI with systematic design tokens
</p>
<Button size="lg">Get Started</Button>
</Stack>
</Container>
</Section>
<Section spacing="lg">
<Container>
<Stack gap={12}>
<h2 className="text-3xl font-bold">Features</h2>
<Grid cols={3} gap={8}>
<FeatureCard />
<FeatureCard />
<FeatureCard />
</Grid>
</Stack>
</Container>
</Section>
<Section spacing="md" background="neutral">
<Container>
<TestimonialCarousel />
</Container>
</Section>
</>
);
}
Optical Spacing Adjustments
Sometimes mathematical spacing needs visual adjustment:
// tokens/optical-spacing.ts
export const opticalSpacing = {
// Negative margin for optical alignment of rounded elements
roundedNegative: '-2px',
// Extra spacing for elements with shadows
shadowCompensation: '4px',
// Text optical centering
textCenterAdjust: '-0.05em', // Pull text up slightly for optical center
// Icon-to-text optical spacing
iconTextGap: {
opticalLeft: '0.5rem', // Slightly more when icon is on left
opticalRight: '0.375rem', // Slightly less when icon is on right
},
} as const;
Apply in components:
export function IconButton({ icon, children }: { icon: ReactNode; children: ReactNode }) {
return (
<button className="flex items-center gap-2">
<span className="flex-shrink-0">{icon}</span>
{/* Optical adjustment: text pulls slightly left toward icon */}
<span style={{ marginLeft: '-2px' }}>{children}</span>
</button>
);
}
Using FramingUI for Spacing Systems
FramingUI provides battle-tested spacing components:
import { Stack, Inline, Grid, Section, Container } from 'framingui';
export function Dashboard() {
return (
<Container>
<Section spacing="lg">
<Stack gap={8}>
<Inline justify="between">
<h1>Dashboard</h1>
<Button>Create</Button>
</Inline>
<Grid cols={4} gap={6}>
<MetricCard />
<MetricCard />
<MetricCard />
<MetricCard />
</Grid>
</Stack>
</Section>
</Container>
);
}
This eliminates spacing decisions while maintaining flexibility.
Key Takeaways
Systematic spacing creates visual rhythm:
- Base-unit scale - Mathematical progression (4px or 8px base)
- Semantic tokens - Named spacing for specific contexts
- Layout primitives - Stack, Inline, Grid components
- Responsive spacing - Breakpoint-specific values
- Optical adjustments - Visual fine-tuning when needed
Spacing isn't arbitrary whitespace—it's architectural structure.
Start with a base-unit scale. Define semantic spacing tokens. Build layout primitives. Your UI will have consistent rhythm and hierarchy without manual spacing tweaks.