v0.5

Spacing

Responsive, semantic spacing tokens for layout, section, and component rhythm.

CreateUI ships a set of semantic spacing tokens. You write one class, such as gap-layout-xl, and it resolves to a different value at every breakpoint (48px on mobile, 64px on tablet, 128px on desktop). The responsive curve lives inside the token, so you stop hand-tuning spacing per screen size and the whole product keeps a consistent rhythm.

The problem it solves

Responsive spacing usually means stacking breakpoint variants on every element. It is verbose, easy to get wrong, and impossible to change globally.

Before: manual breakpoints on every element
<section className="gap-12 p-4 md:gap-16 md:p-8 xl:gap-32 xl:p-16">...</section>

With tokens, the same intent is two classes:

After: one token per dimension
<section className="gap-layout-xl p-layout-md">...</section>

The win is not just shorter class lists:

  • One source of truth. The mobile, tablet, and desktop values are defined once per token. Every surface that uses gap-layout-xl scales identically.
  • Global changes are one edit. Want page rhythm to feel tighter on tablet? You change the token, not hundreds of md: variants spread across the codebase.
  • No drift. Designers and engineers reference the same named scale, so spacing stays on-system instead of becoming a pile of one-off gap-7 values.

The three scales

Spacing is split into three scales by what sits on either side of the gap. Pick the scale that matches the size of the things you are spacing.

ScaleUse it forExample
layoutSpace between and around page regions: gaps between sections, page gutters.gap-layout-md, px-layout-sm
sectionSpace inside a section: header to body, stacks of cards, grouped rows.gap-section-lg, py-section-md
componentInternal padding and gaps of a single component: a button, a chip, a field.p-component-md, gap-component-sm

Rule of thumb: the bigger the things on either side of the gap, the higher the scale. A gap between two full sections is layout. A gap between two lines of text inside a card is component.

How a token responds

Every token carries three values, one per breakpoint range:

  • Mobile: 320 to 767px
  • Tablet: 768 to 1279px
  • Desktop: 1280px and up

A single utility class emits all three. For example, gap-layout-xl produces:

/* gap-layout-xl resolves to */
gap: 48px; /* mobile  (320-767px)  */
gap: 64px; /* tablet  (768-1279px) */
gap: 128px; /* desktop (1280px+)    */

You never write the breakpoints yourself. The token already knows its full curve.

Token reference

Values below are the resolved pixels at each breakpoint, on the default --spacing base of 4px. Each bar is drawn at its live token width, so it resizes with the viewport exactly as the token does.

layout
layout-xl
48px · 64px · 128px
layout-lg
32px · 48px · 96px
layout-md
16px · 32px · 64px
layout-sm
12px · 24px · 48px
layout-xs
8px · 16px · 32px
layout-none
0
section
section-xl
20px · 32px · 48px
section-lg
16px · 20px · 32px
section-md
12px · 16px · 24px
section-sm
12px · 16px · 16px
section-xs
8px · 12px · 12px
section-none
0
component
component-xl
12px · 16px · 24px
component-lg
12px · 16px · 16px
component-md
8px · 12px · 12px
component-sm
8px
component-xs
4px
component-none
0

Layout

Page-level spacing.

TokenMobileTabletDesktop
layout-xl48px64px128px
layout-lg32px48px96px
layout-md16px32px64px
layout-sm12px24px48px
layout-xs8px16px32px
layout-none000

Section

Section-level spacing.

TokenMobileTabletDesktop
section-xl20px32px48px
section-lg16px20px32px
section-md12px16px24px
section-sm12px16px16px
section-xs8px12px12px
section-none000

Component

Component-internal spacing.

TokenMobileTabletDesktop
component-xl12px16px24px
component-lg12px16px16px
component-md8px12px12px
component-sm8px8px8px
component-xs4px4px4px
component-none000

Using the tokens

The token name plugs into any Tailwind spacing utility. Tailwind v4 generates the full set automatically, so all of these work:

<div className="p-component-md" />        {/* padding */}
<div className="px-layout-sm py-layout-md" /> {/* axis padding */}
<div className="gap-section-lg" />        {/* flex / grid gap */}
<div className="mt-section-md" />         {/* margin */}
<div className="space-y-component-sm" />  {/* child spacing */}

The same applies to m-, mx-, my-, mt-, gap-x-, gap-y-, and the rest of the spacing utilities.

Choosing semantic vs raw spacing

Semantic tokens scale across breakpoints. That is exactly what you want when the design opts into responsive spacing, and exactly what you do not want for a fixed, static gap.

Real-world example

The three scales compose naturally: layout frames the section, section spaces the groups inside it, and component handles the internals of each card.

<section className="gap-layout-md px-layout-sm py-layout-lg flex flex-col items-center">
  <header className="gap-section-sm flex flex-col">
    <h2>Latest posts</h2>
    <p>Updates from the team.</p>
  </header>
 
  <div className="gap-section-md grid">
    <article className="gap-component-lg p-component-xl flex flex-col">
      <h3>Post title</h3>
      <p>Excerpt...</p>
    </article>
  </div>
</section>

Resize the viewport and every gap retunes itself. You did not write a single breakpoint variant.

How it works

You do not need this to use the tokens, but it helps to know the numbers are generated, not hand-written.

The pipeline runs from a single data file to Tailwind utilities:

  1. apps/v4/lib/createui-spacing.ts defines TOKENS. Responsive values use the r(mobile, tablet, desktop) helper; a single number means a flat value.

    apps/v4/lib/createui-spacing.ts
    export const TOKENS = {
      "layout-xl": { value: r(12, 16, 32) }, // 48px | 64px | 128px
      "component-sm": { value: r(2, 2, 2) }, // 8px (flat)
      "component-none": { value: 0 },
    }
  2. apps/v4/registry/config.ts consumes the tokens via buildSpacingCssVars(), turning each into a responsive value string.

  3. The CLI (packages/createui/src/utils/updaters/update-css-vars.ts) splits each token across breakpoints, writing the desktop value to :root and the smaller values into max-width media queries.

    generated in app globals.css
    :root {
      --spacing-layout-xl: calc(var(--spacing) * 32); /* 128px */
    }
    @media (max-width: 1279px) {
      :root {
        --spacing-layout-xl: calc(var(--spacing) * 16);
      } /* 64px */
    }
    @media (max-width: 767px) {
      :root {
        --spacing-layout-xl: calc(var(--spacing) * 12);
      } /* 48px */
    }
  4. An @theme inline block registers each --spacing-* variable, and Tailwind v4 auto-generates the gap-*, p-*, m-* utilities from it.

Programmatic access

For Storybook, tests, or runtime logic, createui-spacing.ts exports helpers to resolve tokens in JavaScript.

import {
  resolveToken,
  tokenNames,
  tokensByCategory,
  type BreakpointName,
} from "@/lib/createui-spacing"
 
resolveToken("layout-xl", "desktop") // "calc(var(--spacing) * 32)"
resolveToken("layout-xl", "mobile") // "calc(var(--spacing) * 12)"
resolveToken("layout-xl") // defaults to "desktop"
 
tokenNames() // ["layout-xl", "layout-lg", ...]
tokensByCategory() // { layout: [...], section: [...], component: [...] }

BreakpointName is "mobile" | "tablet" | "desktop".

Changing or adding a token

Edit the token data

Update TOKENS in apps/v4/lib/createui-spacing.ts. Use r(mobile, tablet, desktop) for a responsive value, or a single number for a flat one.

apps/v4/lib/createui-spacing.ts
export const TOKENS = {
  "section-2xl": { value: r(8, 12, 16) }, // 32px | 48px | 64px
}

Rebuild the registry

Run the registry build so the generated CSS variables pick up the change.

pnpm registry:build

Regenerate in consumer projects

In a project that uses CreateUI, createui init regenerates the CSS variables and media queries from the updated tokens.

pnpm dlx @create-ui/cli init