Organizing content

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.

The container, and escaping it

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.

A band running to both edges of the container.

Normal content again, in the reading column.

Relevant code
<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-container
The page shell: three grid tracks, a gutter on each side of a content track at --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-full
On a direct child of .ds-container. The band spans the full track, and its own padding brings its contents back onto the content track.
  • It is a grid track, not a negative margin. The calc(-50vw + 50%) trick overhangs by exactly the scrollbar's width, because 100vw includes the scrollbar and the document width does not.
  • Only a direct child can escape. .ds-full works on children of .ds-container; nested deeper it does nothing, because it has no grid to address.

Page zones

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.

The thing the page is about, on the primary ground.

Closing material, on the secondary ground.

Relevant code
<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-zone-secondary
Goes on .ds-container and becomes the page ground, --ds-zone-secondary-bg. Everything outside the primary band sits on it.
.ds-zone-primary
Goes on one .ds-full band. It sets the --ds-surface ground and the band’s own top and bottom padding. A page has one primary zone.

Cards and grouped sections

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.

When a card earns its box

Sets of parallel items; editable "section card" containers in forms; anything selected, compared, or repeated.

Relevant code
<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>
.ds-card
The box: white surface, hairline border, 8px radius, padding from the scale. Its first and last children carry no outer margin, because the padding is the space at the edges. It sets position: relative, so a margin note or an anchored pill measures from it.
.ds-card-grid
Cards side by side. The count follows the room: the grid drops to one column when a card can no longer hold its minimum width, with no breakpoint.
.ds-card > .ds-acc:last-child
An accordion that is the card's last child runs flush to the card's foot, the way the code panels on this page do.

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:

  • Public — Card Grey tint: a functional tool block (validated: the citation-export block in the merged mockup — copy actions live there; browsable cards around it stay white).
  • Internal — lime wash: the editable workbench. Edit affordances, links, and chips for the record you are working on live inside it.
  • Internal — warm grey wash with a lime top bar: read-only reference. System facts (IDs, logins); look, do not touch; lower priority.
  • Do not tint a card just for variety — an unexplained tint falsely promises interactivity.

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.

Notes

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.

Tone

Grey, the default. Guidance: how to write something, when to reach for one option over another. Nothing here is a rule you can fail.

Accessibility essentials

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.

Internal variant

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.

Relevant code
<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>
.ds-note
The box, on a <div> in the flow of the page. On its own it is the grey register: guidance.
.ds-note-label
The tab, and the note's first child. The label is a heading, at whatever level the page is up to: .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.
.ds-note--essential
The blue register: a requirement.
.ds-note--internal
The lime register: how the component differs in internal tools.
.ds-note-gotcha
A ruled-off warning at the foot of the note, on a <p> whose first child is a <strong> lead.
  • Three registers, and the set is closed on purpose. Adding one is three custom properties — --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.
  • No left edge and no icon. Both belong to the alert, and the tab already does the work of saying what kind of box this is — in words, so nothing has to be read out of a colour.

Marginalia

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.

Note

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.

Relevant code
<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>
.ds-marginalia
The note, a <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-marginalia-body
The text, with .ds-annotation for the voice. In the margin it has no chrome; folded, it opens as a small panel under the mark.
.is-sr-only
Required on the mark. The icon is aria-hidden, so without it the control announces as nothing.
  • It opens without JavaScript. The fold is the native disclosure.
  • The margin appears at 1314px. The page width plus a note and its gaps on each side. Below that, every note is folded.
  • A note is not a rule. A reader on a narrow screen may never open it, so anything a builder must not get wrong goes in a note block or in the prose.

Dividers

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.

Search Submit Donate
Relevant code
<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>
<hr>
The preferred form. Styled by the foundation, no class needed, and it carries role="separator" for free.
.ds-divider
The class form, for where an <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>.
.ds-divider--tight
Between items in a group rather than between sections.
.ds-divider--flush
No margin at all; the caller owns the spacing.
.ds-divider--vertical
For a row of controls. Height is in em, so it tracks the row's own text instead of needing a value per context.

Which to reach for

Reach forWhen
NothingThe 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 dividerSpace 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 cardThe group is an object on the page rather than a region of it, and needs an edge on all four sides to say so.

Linking to a section

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.

A section heading

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.

Relevant code
<!-- 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>
.section-title
On the <h2> or <h3> that starts a section. verification/gen-anchors.py writes its id, and anchors.js appends the control.
.ds-anchor
The control, a <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.
  • The ids are written into the HTML, not added by a script. 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.
  • The control is a button, not a link. An <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.
  • An id follows its heading, so rewording a heading changes its anchor. That is the right trade while nothing links in from outside; re-run the generator after editing a heading and the check will tell you if you forget.
  • Its accessible name carries the section. Twelve buttons all called “Copy link to section” are useless in a list of controls, so each says which section it belongs to.

Chrome anchored to content

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.

Rμν − ½R gμν + Λgμν = 8πG Tμν
Relevant code
<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>
.ds-element-pill
The pill: white, rounded, shadowed, and positioned absolutely inside the element it belongs to. The wrapper supplies position: relative; the pill sets no position of its own, so the host sets bottom, left and the centring transform. Hidden at rest.
.is-revealed
Shows the pill. The host adds it while the wrapped content is hovered or holds focus and removes it after; the demo above keeps it on. The stylesheet switches its fade off under prefers-reduced-motion.
<button>, <a>
The actions, as direct children of the pill. The pill gives each a 24px target floor. Their type and colour have no class of their own yet; this page stages them with a docs-only class.

Accessibility essentials

  • One main landmark. Put the page's content in a single <main>. A page with two, or with none, gives a screen-reader user nothing to jump to.
  • Label every aside. A rail is an <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.
  • Headings continue the outline inside a card. A heading inside a container takes the next level down from the section it sits in. Do not skip a level because the content is in a card.
  • Use native disclosure. Build accordions, margin notes and the contents bar on <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.
  • Nothing required lives only in a collapsed panel. Anything legally or practically required, such as a warning or a licence, must also exist in always-visible flow. A closed accordion, a folded margin note and a popover are all places a reader may never open.

Rules

Main column vs. sidebar — what goes where

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.

Main reading column

Time-critical, everyone meets it:

  • Title · authors
  • Version date line ("Submitted … · Revised …")
  • Older-version warning (amber, advisory)
  • Abstract · body

Rail (aside)

Browsable, opt-in:

  • Versions accordion (full history)
  • Paper information
  • Related / Labs

Accordions and other disclosures

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.

Motion and layering