The containers that group arXiv's content: the reading-column + sidebar model, accordions, cards, and chrome that anchors to content (popovers, element pills). The organizing principle throughout: whitespace first, boxes second — a box is a cost, paid only when proximity alone cannot express the grouping. Reference in action: the HTML paper mockup.
Every page sits in .ds-container, a grid with two tracks: a
content track at the reading width, and a full track running to the viewport edges.
Everything lands in the content track unless it asks not to.
Asking not to is .ds-full — a band that runs edge to edge for a coloured
section, a reference zone, or a figure wider than the column. It brings its own contents back onto
the content track with padding derived from the same width the container uses, so the band and the
column cannot drift apart.
Normal content, in the reading column.
Normal content again, in the reading column.
<div class="ds-container">
<p>Normal content, in the reading column.</p>
<div class="ds-full">A band running to both viewport edges.</div>
<p>Normal content again, in the reading column.</p>
</div>
--ds-width-page. Every child lands in the content track. The space between
children is the container's own row-gap, so a section carries no margin of
its own..ds-container. The band spans the full track, and its
own padding brings its contents back onto the content track.calc(-50vw + 50%) trick overhangs by exactly the scrollbar's width, because
100vw includes the scrollbar and the document width does not..ds-full works on children of
.ds-container; nested deeper it does nothing, because it has no grid to address.A long page can tell a reader what kind of content they are in by its ground. The primary zone holds the thing the page is about. The secondary zone holds what introduces or supports it: the opening of the page, and any closing material. The zones are named for their role, because both grounds change in dark mode.
The opening of the page, on the secondary ground.
Closing material, on the secondary ground.
<div class="ds-container ds-zone-secondary">
<header class="ds-page-header">…</header> <!-- secondary: introduces -->
<div class="ds-full ds-zone-primary">
<section>…</section> <!-- primary: the thing itself -->
</div>
<section>…</section> <!-- secondary: supports -->
</div>
.ds-container and becomes the page ground,
--ds-zone-secondary-bg. Everything outside the primary band sits on it..ds-full band. It sets the --ds-surface ground and
the band’s own top and bottom padding. A page has one primary zone.A card is a white surface with a hairline border and 8px radius on the Warm Wash page background — used when content is a peer in a set (context cards, swatch cards, editable form sections). It is not a decoration for single blocks of prose.
Sets of parallel items; editable "section card" containers in forms; anything selected, compared, or repeated.
<div class="ds-card">
<h3>When a card earns its box</h3>
<p>Sets of parallel items; editable "section card" containers in forms; anything selected, compared, or repeated.</p>
</div>
<!-- Peers side by side -->
<div class="ds-card-grid">
<div class="ds-card">…</div>
<div class="ds-card">…</div>
</div>
position: relative, so a margin note or an anchored pill measures from
it.Rows inside a card separate with spacing, never hairline dividers
Dividers belong to tables. If a card's rows need lines to stay apart, make it a table instead.
Card backgrounds: tint is a signal, not decoration
Cards default to white on the Warm Wash page background. A tinted fill is a deliberate signal that the container does something, and the tint family names the surface:
Card, table, or metadata panel?
Three containers, three jobs. A card holds one of several peers in a browsable
set. A table holds many similar records, one per row, scanned down columns to compare. A
metadata panel is a grid of label + value pairs describing
one record (one paper, one user): build it as a definition list (<dl>), separate cells with
spacing by default (faint cell rules only at high density, always lighter than table dividers), and dress it in one of
the two internal tints above. Pattern page: .ds-card--data. Reference in the wild: the Metadata zone on the
admin paper-details mockup and both panels on the
admin user-page mockup.
The proximity rule (see Spacing)
When whitespace does it better: sequential prose and single sections — use the spacing scale's proximity rule instead of a border. Space between sections ≈ 3× the space within a section (16px within, 48px between). When that holds, most boxes become unnecessary.
A box in the flow of a page that names its own register before the reader starts the paragraph: a requirement, a piece of guidance, the way a component differs on the internal surface. The tab is the whole design. It is also what keeps a note from reading as an alert — an alert reports a state as of right now and can be dismissed, a note is always true and stays.
Grey, the default. Guidance: how to write something, when to reach for one option over another. Nothing here is a rule you can fail.
Blue. A requirement — something that has to be true for the pattern to be correct, usually with a WCAG success criterion behind it.
Easy to get wrong
A note can carry a ruled-off warning at its foot with .ds-note-gotcha. Use it for
the mistake the rule keeps being broken by, not as a second paragraph.
Lime. How this component or rule differs in internal tools, shown in the section it belongs to rather than on a separate page. This is the one place a public page may show Access Lime, because here the internal surface is the subject rather than the accent.
<div class="ds-note ds-note--essential">
<h3 class="ds-note-label">Accessibility essentials</h3>
<p>…</p>
<p class="ds-note-gotcha"><strong>Easy to get wrong</strong> …</p>
</div>
<div> in the flow of the page. On its own it is the
grey register: guidance..ds-note-label resets the type completely, so h2 through
h4 all render the same tab. Keeping it a heading keeps the note in the document
outline, which is how a screen-reader user finds it.<p> whose first
child is a <strong> lead.--ds-note-tint, --ds-note-line,
--ds-note-mark — but a colour only earns a register when it has a job.
There is no background-colour utility class, because a tint that a page picks freely teaches
the reader nothing.A short note beside the block it belongs to, in the annotation voice. Where the viewport has a margin the note sits in it, always open. Narrower, it folds to an info mark in the block’s top-right corner that opens it in place. Try it by narrowing this window.
A margin note is one or two sentences. Anything longer is a paragraph, and belongs in the flow.
The block the note belongs to. A demo, a figure, a table: anything in a card.
<div class="ds-card">
<details class="ds-marginalia">
<summary><svg aria-hidden="true">…</svg><span class="is-sr-only">Note</span></summary>
<p class="ds-marginalia-body ds-annotation">The note.</p>
</details>
…
</div>
<details>. Its first child in the block, so it is read
before the thing it annotates. The block supplies position: relative;
.ds-card already does..ds-annotation for the voice. In the margin it has no
chrome; folded, it opens as a small panel under the mark.aria-hidden, so without it the control
announces as nothing.A rule between things. It belongs in the same conversation as the card, because both answer the same question and the answer is usually neither: this system groups with space first, and a line drawn where space would have done adds noise without adding meaning.
A section of content.
The next section, separated by a plain <hr>.
And by .ds-divider--tight, which uses the between-blocks gap
rather than the between-sections one.
<p>A section of content.</p>
<hr>
<p>The next section, separated by a plain <hr>.</p>
<div class="ds-divider ds-divider--tight" aria-hidden="true"></div>
<p>And by .ds-divider--tight, which uses the between-blocks gap rather than the between-sections one.</p>
<!-- In a row of controls -->
<div class="ds-btn-group">
<span>Search</span>
<span class="ds-divider ds-divider--vertical" aria-hidden="true"></span>
<span>Submit</span>
<span class="ds-divider ds-divider--vertical" aria-hidden="true"></span>
<span>Donate</span>
</div>
role="separator" for free.<hr> will not fit:
inside a flex row, or on an element that is already there. It is
decorative, so mark it aria-hidden — a real separator should be an
<hr>.em, so it
tracks the row's own text instead of needing a value per context.Which to reach for
| Reach for | When |
|---|---|
| Nothing | The default. If a larger gap between the groups than within them makes the grouping read, you are done — see the proximity rule on Spacing. |
| A divider | Space alone is not enough, usually because the groups are the same shape and sit close together — rows in a list, a run of controls. |
| A card | The group is an object on the page rather than a region of it, and needs an edge on all four sides to say so. |
Every section heading carries an id derived from its own
text, and .ds-anchor is the control beside it that copies the link. Hover the
heading above, or tab to it, and the control appears.
The control as it appears on hover or focus. Here it is kept visible and copies nothing; the ones beside the headings on this page are live.
<!-- What is written: a heading with the class. The id and the control are generated. -->
<h2 class="section-title">Dividers</h2>
<script src="anchors.js" defer></script>
<!-- What renders, after gen-anchors.py and anchors.js -->
<h2 class="section-title" id="dividers">Dividers
<button type="button" class="ds-anchor" aria-label="Copy link to Dividers" title="Copy link to this section">
<svg viewBox="0 0 24 24" aria-hidden="true">…</svg>
</button>
</h2>
<h2> or <h3> that starts a section.
verification/gen-anchors.py writes its id, and
anchors.js appends the control.<button> that anchors.js adds to every
.section-title with an id. Quiet until wanted: it appears on
hover of the heading and on its own focus, and stays visible on a touch screen. Its
accessible name carries the section, and the copy result is announced in a live
region.verification/gen-anchors.py writes them;
--check fails when a heading has none. A real id works with JavaScript off, works
for a link arriving from another page, and exists when the browser resolves the fragment on
first load — none of which a script running at DOMContentLoaded can
promise.<a href="#here">
inside a heading is a tab stop that goes nowhere the reader wanted to go — they are
already looking at the section. What they want is the address, so it copies it.Two shared components attach lightweight UI to a specific piece of content rather than
to the page. Both are real classes in design-system.css.
Popover panel — .ds-popover
A light-blue tint floating panel anchored to inline elements (citation chips, footnote markers). z-index 110, per the z-layer scale. Its markup and its classes are documented on Progressive disclosure.
Element pill — .ds-element-pill
A small white pill straddling an element's bottom edge (equations, figures) to host its actions — Distill-style anchored chrome. Hidden at rest; revealed on hover/focus of the wrapped element.
<div class="eqn-wrap" style="position: relative;">
<!-- equation -->
<div class="ds-element-pill" style="bottom: -14px; left: 50%; transform: translateX(-50%);">
<button type="button">View TeX</button>
<button type="button">Permalink</button>
<button type="button">Copy</button>
</div>
</div>
position: relative; the pill sets no position
of its own, so the host sets bottom, left and the centring
transform. Hidden at rest.prefers-reduced-motion.<main>. A page with two, or with none, gives a screen-reader user nothing to
jump to.<aside> with an
aria-label that says what it holds, so it announces as a complementary landmark
with a name, not as part of the article.<details> and <summary>: the summary is focusable and
toggles with Enter and Space for free. Do not rebuild them from <div>
elements and click handlers.The placement rule decided in the version-display work generalizes: the main column is what every reader must meet; the rail is what some readers want to browse. Screen-reader flow, mobile stacking, and citation-arrival readers all traverse the main column — a rail may never be opened.
Time-critical, everyone meets it:
Browsable, opt-in:
<aside> with an aria-label — a complementary landmark, not part of the article.An accordion is one of the containers this page is about, but it is also one of several ways a page can show less than it has — and choosing between them is the harder question. Accordions, show-more, and popovers are documented together on Progressive disclosure, with the rule for picking one. The contents bar is a disclosure as well, and its markup and classes are on Progressive disclosure too.
What belongs here is only where they sit: the rail variant
(.ds-acc-rail) is the one built for the sidebar in the column model above. It drops the
rules the default carries, because the rail's own hairline dividers already separate its contents,
and a boundary drawn twice is a boundary drawn wrong.
<dialog> top layer. No ad-hoc z-index values. The scale is kept in DESIGN-POLICIES.prefers-reduced-motion via its own reduce rule (.ds-element-pill { transition: none; }); accordions have no animation to disable.