Skip to content

Spacing System

The theme uses a token-driven spacing system built with SCSS maps and CSS variables. A small numeric scale is the single source of truth; intent-based semantic tokens point at that scale, and consumers reference a semantic name where one fits — never a raw rem value.

The spacing setup has two layers:

  1. Primitives — the raw numeric scale (the only place a rem value is written).
  2. Semantics — intent-based aliases that point at a primitive, optionally per breakpoint.

Components consume spacing through the space() / space-value() functions, which accept either a semantic token or a raw primitive key. Reach for a semantic when one fits the intent; fall back to a primitive for one-off values.

assets/src/scss/abstracts/variables/_spacing.scss
$spacing-scale: (
0: 0,
4: rem(4px),
8: rem(8px),
12: rem(12px),
16: rem(16px),
24: rem(24px),
32: rem(32px),
48: rem(48px),
64: rem(64px),
96: rem(96px),
);

Keyed by the pixel value, valued in the rem equivalent. Reference a step directly when no semantic fits — e.g. space(12).

$spacing-semantic: (
nav-gap: 12,
prose-gap: 16,
);

Each entry is either:

  • a primitive key directly (e.g. nav-gap: 12), or
  • a map with:
    • base — the primitive key used by default.
    • breakpoint — optional responsive overrides, repointing the token at a different primitive key per breakpoint.
section-gap: (
base: 48,
breakpoint: (
tablet: 32,
mobile: 24,
),
),

Breakpoints are defined in abstracts/variables/_media-queries.scss and are max-width (so scale down for smaller screens). This mirrors the typography responsive model.

Two functions resolve a spacing token. Both accept a semantic name or a raw primitive key.

.main-menu {
gap: space(nav-gap); // → var(--space-nav-gap)
}
.toolbar {
gap: space(12); // → var(--space-12)
}
FunctionReturnsUse when
space($name)var(--space-#{$name})the normal case — a runtime CSS variable. Follows responsive overrides automatically.
space-value($name)the literal rem valuea custom property can’t be used (e.g. inside calc() math, or where the value must be known at compile time). Resolves to the token’s base primitive.
.sidebar {
width: calc(100% - space-value(prose-gap)); // → calc(100% - 1rem)
}

A key that matches no primitive and no semantic fails the build:

space(does-not-exist); // ✗ "Unknown spacing token `does-not-exist`."

Validating at compile time means a typo’d token never silently ships as a missing value.

:root {
/* primitives */
--space-0: 0;
--space-4: 0.25rem;
--space-8: 0.5rem;
/* … */
/* semantics → reference a primitive */
--space-nav-gap: var(--space-12);
--space-prose-gap: var(--space-16);
}

Every token generates a CSS variable on :root automatically (from assets/src/scss/generate/_spacing.scss) — you never write these by hand. Semantic variables reference their primitive, so retuning a primitive cascades everywhere.

When a semantic token defines breakpoint overrides, the generator re-emits only that variable inside the matching media query — widest-first so the narrowest wins the cascade:

@media screen and (max-width: 46rem) {
:root {
--space-section-gap: var(--space-24);
}
}

Anything using space(section-gap) follows along with no recompile of consumers.

  1. Open the token file

    Edit assets/src/scss/abstracts/variables/_spacing.scss.

  2. Add a primitive (only if the value isn’t on the scale yet)

    $spacing-scale: (
    // …
    20: rem(20px),
    );
  3. Add a semantic alias

    $spacing-semantic: (
    nav-gap: 12,
    prose-gap: 16,
    card-padding: (
    base: 24,
    breakpoint: (
    mobile: 16,
    ),
    ),
    );
  4. Use it via space()

    .card {
    padding: space(card-padding);
    }
  1. Open the Spacing / Layout board in Figma
  2. Copy the raw step values into $spacing-scale
  3. Map each repeated usage to a semantic token by intent (nav-gap, card-padding, section-gap), not by number
  1. Go page by page and collect padding / margin / gap values
  2. Identify repeating values and group them by intent
  3. Add the distinct values as primitives, then create semantic aliases for each intent
  4. Replace hardcoded values with space(...)