v1.0

Spacing

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

Create UI 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. The responsive curve lives inside the token, so you stop hand-tuning spacing per screen size and the whole product keeps a consistent rhythm.

There are 18 of them across three scales. Thirteen scale with the viewport and five hold one value on purpose.

Responsive · 3 values
Mobile320 to 767px
48px
Tablet768 to 1279px
64px
Desktop1280px and up
128px

One class, three values. The outlined panel is your screen right now, so resize the window and watch it move.

2 media queries in the system, 0 in your markup

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. The bigger the things you are separating, the higher the scale.

LayoutBetween two full sectionsgap-layout-lg
9696 pixels on Desktop
4848 pixels on Tablet
3232 pixels on Mobile
SectionBetween a header and its cardsgap-section-xl
4848 pixels on Desktop
3232 pixels on Tablet
2020 pixels on Mobile
ComponentBetween two lines in a cardgap-component-xl
2424 pixels on Desktop
1616 pixels on Tablet
1212 pixels on Mobile

Token reference

Each scale is drawn against its own largest step, so the steps within a scale compare directly and the card header says what a full track is worth. The head of each bar is the mobile value and each step after it is what the token gains as the screen grows, so the whole curve reads at a glance. Clicking a token name copies its gap-* class.

layoutMobileTabletDesktop
486412848px on Mobile. 64px on Tablet. 128px on Desktop.
32489632px on Mobile. 48px on Tablet. 96px on Desktop.
16326416px on Mobile. 32px on Tablet. 64px on Desktop.
12244812px on Mobile. 24px on Tablet. 48px on Desktop.
816328px on Mobile. 16px on Tablet. 32px on Desktop.
0000px on Mobile. 0px on Tablet. 0px on Desktop.
sectionMobileTabletDesktop
20324820px on Mobile. 32px on Tablet. 48px on Desktop.
16203216px on Mobile. 20px on Tablet. 32px on Desktop.
12162412px on Mobile. 16px on Tablet. 24px on Desktop.
12161612px on Mobile. 16px on Tablet. 16px on Desktop.
812128px on Mobile. 12px on Tablet. 12px on Desktop.
0000px on Mobile. 0px on Tablet. 0px on Desktop.
componentMobileTabletDesktop
12162412px on Mobile. 16px on Tablet. 24px on Desktop.
12161612px on Mobile. 16px on Tablet. 16px on Desktop.
812128px on Mobile. 12px on Tablet. 12px on Desktop.
8888px on Mobile. 8px on Tablet. 8px on Desktop.
4444px on Mobile. 4px on Tablet. 4px on Desktop.
0000px on Mobile. 0px on Tablet. 0px on Desktop.

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.

Reading a token at runtime

The tokens reach your project as CSS custom properties, so Storybook, tests, and runtime logic read them off the document rather than importing a module.

const layoutXl = getComputedStyle(document.documentElement)
  .getPropertyValue("--spacing-layout-xl")
  .trim()

The value you get back is the one for the current viewport, because the two max-width blocks have already resolved.

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. lib/createui-spacing.ts defines TOKENS. Responsive values use the r(mobile, tablet, desktop) helper; a single number means a flat value.

    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. registry/config.ts consumes the tokens via buildSpacingCssVars(), turning each into a responsive value string.

  3. The CLI splits each token across breakpoints, writing the desktop value to :root and the smaller values into the stylesheet's two 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.

Changing or adding a token

Override the variable

Set the --spacing-* custom property in your own globals.css. Desktop goes on :root; the smaller ranges go in the two max-width blocks the CLI already wrote.

globals.css
:root {
  --spacing-section-2xl: calc(var(--spacing) * 16); /* 64px */
}
@media (max-width: 1279px) {
  :root {
    --spacing-section-2xl: calc(var(--spacing) * 12);
  } /* 48px */
}

Register a brand-new name

Tailwind generates utilities only from names declared in @theme inline, and the CLI writes that block with the shipped tokens enumerated one by one. Overriding an existing token needs nothing more, but a new name has to be added:

globals.css
@theme inline {
  --spacing-section-2xl: var(--spacing-section-2xl);
}

Skip this and gap-section-2xl silently emits no rule at all.

Or pull the current scale

Re-running init rewrites the variables and media queries from the shipped tokens, which is the way to take an updated scale rather than keep a local edit.

pnpm dlx @create-ui/cli init
Every one of these tokens ships with the components.Install Create UI