One 4px-based / 8-point scale, and one rule about how to use it. Every gap on
every arXiv page comes from these seven steps — no off-scale values (14, 26, …).
Written spec: DESIGN-POLICIES.md (Spacing);
tokens: --ds-space-1…--ds-space-12 in design-system.css.
Seven steps, each roughly 1.5× the last. The gaps in the bars below are the
token values — the page pulls them live from --ds-space-*. Production code
uses them exactly as written — --ds-space-4 everywhere, with no per-codebase renaming.
| Token | Value | Drawn at size |
|---|---|---|
--ds-space-1 | 0.25rem (4px) | |
--ds-space-2 | 0.5rem (8px) | |
--ds-space-3 | 0.75rem (12px) | |
--ds-space-4 | 1rem (16px) | |
--ds-space-6 | 1.5rem (24px) | |
--ds-space-8 | 2rem (32px) | |
--ds-space-12 | 3rem (48px) |
/* A stop is used as written: a token, in rem, never a pixel value */
.ds-card {
padding: var(--ds-space-4) var(--ds-space-6); /* 16px 24px at a 16px root */
}
The gap between sections should be clearly larger than the gap within one — aim for ~3× (e.g., 16px within, 48px between). This is what makes grouping read without borders or boxes. Same content, same words, both examples below — only the spacing differs.
Every gap in this demo sits on a container as gap, so no child carries a margin of its own.
✓ Two groups, read instantly
✗ One undifferentiated list
/* One named break per kind of gap, on the container, never on a child */
.record { display: grid; gap: var(--ds-space-section); } /* between groups */
.record-group { display: grid; gap: var(--ds-space-block); } /* between rows in a group */
.record-row { display: grid; gap: var(--ds-space-tight); } /* a label and its value */
Three tokens set the shape of every page: the width of the content column, the gutter that keeps it off the viewport edge, and the space the container puts between one region and the next. The container that reads them is documented on Organizing content.
The demo is a real container, so the tracks it draws are the live token values.
A band that runs edge to edge. Its own padding is --ds-gutter, so this
text still sits on the content track.
The next region. The container leaves --ds-space-section above it, and
the ground on either side of this card is the gutter.
<div class="ds-container">
<header class="ds-page-header">…</header>
<section>…</section> <!-- --ds-space-section above it, from the container -->
<section>…</section>
</div>
/* A page that needs a different width changes the one token;
it never adds a second width beside it. Here: a dense internal
tool with tables that cannot compress. */
:root { --ds-width-page: 1080px; }
--ds-space-6, 24px. The least space between the content track and the
viewport edge. A .ds-full band uses the same token as its inline padding,
so the band and the column cannot drift apart.row-gap: the space between one direct child and
the next. A section carries no margin of its own.rem, so every gap
grows with the reader's text size (WCAG 1.4.4). At 400% page zoom the page is 320 pixels
wide (WCAG 1.4.10): do not set a gap or a width that forces the page to scroll
sideways.<fieldset> or a landmark, so the grouping is in the markup as well as in
the gap.Reach for a token, never a hand-picked pixel. If a layout seems to want 14px or 26px, it wants an existing step (12 or 24) — or the design is fighting the grid. New values are a change to DESIGN-POLICIES, not a local override.
Grouping is carried by contrast in spacing, not by lines. Reach for the named
break — --ds-space-block within, --ds-space-section between — rather than
the raw steps they map to. Block is 2× tight and section is 3× block; below about
1.5× the eye cannot tell the two apart and the grouping silently fails. When a border
feels necessary to separate two blocks, first try widening the gap.
The scale is expressed in rem, so spacing scales with the reader's text size.
A fixed gap beside growing text compresses the rhythm exactly for the person who asked for
more room, and the ratio between within and between is what carries meaning, so it
has to survive.
Pages use --ds-space-tight (inside a group), --ds-space-block (between
blocks in a section) or --ds-space-section (between sections) — never a pixel
value, and never a raw --ds-space-N. The raw scale is the palette; these are the
roles, the same way colours are named for what they do rather than what hue they are.
Changing what a break means then moves every page together.
A section does not know what follows it, so it cannot know how far away that should be.
Put the space on the container as gap, not on each child as a margin: gap never
collapses, cannot be undone by a child, and needs no last-child resets. If a system needs
:last-child { margin-bottom: 0 }, that is not a fix, it is a symptom that the
space is owned by the wrong thing.
Prose flow inside a section stays element-owned, because a heading's space depends on its own size, which a container cannot know. Two mechanisms, at two clearly separated levels, never competing at the same one.
“How far apart do these sit” means the same thing whether the space comes from a margin or a gap, and whether it runs down the page or across it. So gap between siblings uses the rhythm above, on either axis.
One exception: the space between a glyph and its label inside a single control is
part of the control rather than layout. Use --ds-gap-glyph (0.5em),
which scales with that control's own type — the same exemption already given to button
padding.
Buttons placed as bare siblings are spaced by the HTML whitespace between the tags —
narrower than any deliberate value, and it looks like a bug because it is one. Use
.ds-btn-group, or --stack for the vertical axis and
--end for a form's action area.
There is one name per stop, --ds-space-6, and every codebase uses it exactly as written. No per-repository prefixes and no mapping tables: a mapping kept in another repo is invisible to everything here.