A Practical Approach to CSS Spacing Tokens

CSS custom properties can do more than save you from repeating numbers. With a small spacing scale and a few contextual tokens, you can make layout changes without hunting through your stylesheet.

Have you ever needed to change the spacing across a website and found yourself searching through your stylesheet for every occurrence of 24px, 32px, or 48px?

It happens quite easily. You start with a few CSS rules, and before long, the same spacing values appear in your hero section, cards, headings, and content blocks. When the design changes, you have to find all those declarations and update them individually.

CSS custom properties can make this easier. But rather than simply replacing every pixel value with a variable, let’s look at how to build a spacing system that makes sense for a real website.

Start by defining your spacing values

Imagine you’re building a website with a hero section, an about section, and a services section. You want these sections to have generous vertical padding, while cards and smaller content blocks use less space.

You might start with something like this:

.hero {
  padding: 64px 24px;
}

.about {
  padding: 64px 24px;
}

.services {
  padding: 64px 24px;
}

.card {
  padding: 24px;
}

This works, but notice how the same values appear in multiple places. If you decide that the sections need less vertical padding, you’ll have to update each rule.

Instead, define reusable spacing values at the root level using CSS custom properties:

:root {
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-2xl: 48px;
  --space-3xl: 64px;
}

The :root selector represents the document’s root element. Defining your variables here makes them available throughout the page, so you can use the same spacing values in different components without redefining them.

Now your CSS can use those variables instead of hardcoded values:

.hero {
  padding: var(--space-3xl) var(--space-lg);
}

.about {
  padding: var(--space-3xl) var(--space-lg);
}

.services {
  padding: var(--space-3xl) var(--space-lg);
}

.card {
  padding: var(--space-lg);
}

If you want to change the spacing scale, you can update the values in one place. For example, changing --space-3xl from 64px to 48px reduces the vertical padding of all three sections.

That’s already useful. However, there’s a catch.

The problem with using only generic spacing variables

Let’s say you want to reduce the padding of your main sections, but leave the spacing inside your cards unchanged.

You could change --space-3xl to 48px. That works for the three sections in our example, but imagine that you’ve also used --space-3xl for a large gap above your footer or between two content blocks. Those elements will change too.

You could go through the stylesheet and adjust individual declarations, but then you’re back to managing spacing one element at a time.

The underlying problem is that --space-3xl tells you how much space you’re using, but not why you’re using it.

This is where it helps to introduce another layer of variables: contextual spacing tokens.

Use contextual tokens for elements that should change together

Instead of assigning a generic spacing variable directly to every element, create variables that describe the purpose of the spacing:

:root {
  /* Base spacing values */
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-2xl: 48px;
  --space-3xl: 64px;

  /* Contextual spacing tokens */
  --section-padding-y: var(--space-3xl);
  --section-padding-x: var(--space-lg);
  --card-padding: var(--space-lg);
}

The contextual variables refer to the base spacing values. This means you can reuse your spacing scale while keeping control over different parts of the design.

Apply them to the page:

.hero,
.about,
.services {
  padding-block: var(--section-padding-y);
  padding-inline: var(--section-padding-x);
}

.card {
  padding: var(--card-padding);
}

Now suppose the main sections feel too spacious, but the cards look fine. You only need to change one variable:

:root {
  --section-padding-y: var(--space-2xl);
}

All three sections now use 48px of vertical padding. The cards retain their 24px padding because they use a different contextual token.

Where else can you use contextual spacing tokens?

Once you start looking for recurring patterns, you’ll probably find several opportunities to use them.

For example, a card might have a heading, a paragraph, and a button. You want to control the gaps between these elements independently of the card’s outer padding.

:root {
  --card-padding: 24px;
  --card-content-gap: 12px;
  --card-grid-gap: 24px;
}

.card {
  padding: var(--card-padding);
}

.card__content {
  display: flex;
  flex-direction: column;
  gap: var(--card-content-gap);
}

.card-grid {
  display: grid;
  gap: var(--card-grid-gap);
}

Here, each variable controls a different relationship:

  • --card-padding controls the space inside the card.
  • --card-content-gap controls the space between elements within the card.
  • --card-grid-gap controls the space between cards.

If the cards are too close together, you can increase the grid gap without affecting the spacing inside each card.

You can use the same principle for form fields, section headings, navigation items, and other recurring components. Create a shared token when you expect those elements to change together, not simply because they happen to use the same value today.

Make spacing adjustments easier on mobile

You can also use contextual tokens to manage responsive spacing. Your desktop layout might look good with 64px of vertical section padding, but that could be excessive on a mobile screen.

:root {
  --section-padding-y: 64px;
  --section-padding-x: 24px;
}

@media (max-width: 768px) {
  :root {
    --section-padding-y: 40px;
    --section-padding-x: 16px;
  }
}

You don’t need to rewrite the rules for .hero, .about, and .services. They’ll automatically use the updated values because they all reference the same contextual tokens.

You can also use clamp() when you’d prefer spacing to scale gradually with the viewport rather than change at a specific breakpoint:

:root {
  --section-padding-y: clamp(40px, 6vw, 64px);
}

This keeps the vertical padding between 40px and 64px, with the middle value adapting to the viewport width.

Don’t turn every spacing value into a variable

It’s easy to get carried away when building a token system. You might end up creating a separate variable for every heading, paragraph, and component. That isn’t necessarily helpful.

If several sections use the same spacing for the same reason, a shared contextual token makes sense:

:root {
  --section-heading-gap: 16px;
}

.hero__title,
.about__title,
.services__title {
  margin-block-end: var(--section-heading-gap);
}

On the other hand, don’t force unrelated elements to share a token just because their spacing values happen to match. A heading’s bottom margin and a card’s internal padding might both be 24px, but they serve different purposes. You may want to change one without affecting the other.

Start with a small spacing scale, look for patterns in your actual layout, and introduce contextual tokens where they make future changes easier. You don’t have to anticipate every possible layout variation before writing the CSS.

A simple rule to follow

Think of your spacing system in two layers.

Your base tokens define the available values:

--space-lg: 24px;
--space-2xl: 48px;

Your contextual tokens define where those values are used:

--card-padding: var(--space-lg);
--section-padding-y: var(--space-2xl);

When you need to change the spacing of a particular design pattern, update its contextual token. When you want to adjust the underlying spacing scale, change the base values and review the impact.

This small distinction can make a big difference as a stylesheet grows. Instead of searching through dozens of declarations every time the design changes, you have a clear place to start.

CSS tutorial · Custom properties · Layout and spacing