Modal dialogs

A window that takes the page away until it is answered or dismissed. The most expensive thing this system can put in front of a reader, and the one with the most ways to build it wrong — so it is a native <dialog>, which gets four of the five hard parts right without being asked.

The modal dialog

Both buttons open the real .ds-modal. Try Escape, try tabbing past the last control, and try scrolling the page behind them.

A decision

Withdraw this submission?

Withdrawing replaces the paper with a withdrawal notice. The submission stays in the record and keeps its identifier — arXiv is a permanent archive, so nothing is removed.

This cannot be undone from here.

Relevant code
<button class="ds-btn ds-btn-primary" type="button" data-open="demo-confirm">A decision</button>

<dialog class="ds-modal" id="demo-confirm" aria-labelledby="demo-confirm-title">
  <div class="ds-modal-header">
    <h2 class="ds-modal-title" id="demo-confirm-title">Withdraw this submission?</h2>
    <button type="button" class="ds-close" data-close>
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
      <span class="is-sr-only">Close</span>
    </button>
  </div>
  <div class="ds-modal-body">
    <p>Withdrawing replaces the paper with a withdrawal notice. The submission stays in the record and keeps its identifier — arXiv is a permanent archive, so nothing is removed.</p>
    <p>This cannot be undone from here.</p>
  </div>
  <div class="ds-modal-footer">
    <button class="ds-btn ds-btn-text ds-modal-footer-start" type="button" data-close>Cancel</button>
    <button class="ds-btn ds-btn-primary" type="button" data-close>Withdraw</button>
  </div>
</dialog>
// Open and close. Focus trap, focus restore, Escape and stacking are
// the element's own doing; the scroll lock is in the stylesheet.
document.getElementById('demo-confirm').showModal();
document.getElementById('demo-confirm').close();
.ds-modal
The dialog, on a native <dialog> element opened with showModal(). Sized against the viewport rather than its content, because a modal that outgrows the window puts its own actions out of reach.
aria-labelledby="…"
Required on the <dialog>, pointing at the title. It is the one thing the native element cannot infer, and without it the dialog announces with no name at all.
.ds-modal-header, .ds-modal-body, .ds-modal-footer
The three regions, in this order. Header and footer hold their size; the body takes what is left and scrolls, which is what keeps the title and the actions on screen when the content is long.
.ds-modal-title
The heading inside the header. It carries the id that aria-labelledby points at.
.ds-modal-footer-start
Pushes one control to the opposite end — Cancel reads better away from the action it undoes.
.ds-close
The same close control as everywhere else; see Buttons. It sits inside .ds-modal-header, after the title.
data-open="…", data-close
Hooks for this page's demo script, which finds the button that opens a dialog and the controls that close it. The stylesheet gives them no meaning; your own script may use any hook it likes.

Long content

What reading data is collected

The header and the actions stay put; only this region scrolls. That is the reason the dialog caps its own height — a modal that grows with its content eventually puts its own buttons below the bottom of the window, where the reader cannot reach the thing they opened it to do.

Which papers you open, in what order, and for how long. Nothing you type. Nothing from outside arXiv.

Reading data is never shared publicly or incorporated into any public system in a personally identifiable form.

You can withdraw consent at any time, and withdrawing removes the data already collected.

Research use includes building recommendation systems that work without profiling individual readers.

Keep scrolling: the header above and the footer below have not moved.

Consent is recorded against your account, not against a browser, so it follows you between devices and is not lost when you clear cookies.

Data is retained for as long as the research programme runs, and deleted within thirty days of it ending.

Researchers receive the data under an agreement that forbids re-identification and forbids passing it on.

arXiv staff can see aggregate figures. Nobody, inside arXiv or outside it, browses one person's reading history.

If you have questions that this does not answer, the privacy policy is the longer version, and it is written to be read rather than agreed to.

This is the last paragraph. The header above and the footer below have still not moved.

Relevant code
<button class="ds-btn" type="button" data-open="demo-long">Long content</button>

<dialog class="ds-modal" id="demo-long" aria-labelledby="demo-long-title">
  <div class="ds-modal-header">
    <h2 class="ds-modal-title" id="demo-long-title">What reading data is collected</h2>
    <button type="button" class="ds-close" data-close>
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
      <span class="is-sr-only">Close</span>
    </button>
  </div>
  <div class="ds-modal-body">
    <p>The header and the actions stay put; only this region scrolls. That is the reason the dialog caps its own height — a modal that grows with its content eventually puts its own buttons below the bottom of the window, where the reader cannot reach the thing they opened it to do.</p>
    <p>Which papers you open, in what order, and for how long. Nothing you type. Nothing from outside arXiv.</p>
    <!-- … more paragraphs … -->
  </div>
  <div class="ds-modal-footer">
    <button class="ds-btn ds-btn-text" type="button" data-close>No thanks</button>
    <button class="ds-btn ds-btn-primary" type="button" data-close>I agree</button>
  </div>
</dialog>
.ds-modal-body
Takes whatever height the header and footer leave and scrolls inside it. Nothing to add: the same markup as a short dialog, with more in the body.

Accessibility essentials

  • Use the native element. Build every modal on a <dialog class="ds-modal"> and open it with showModal(). Never use show(): it opens a non-modal dialog, with no backdrop, no inert page and no focus trap — every guarantee quietly absent, on markup that looks correct. Never build one from a <div role="dialog">, which starts with none of those guarantees either.
  • Name the dialog. Put aria-labelledby on the <dialog>, pointing at the id of its .ds-modal-title. It is the one thing the native element cannot infer, and without it the dialog announces with no name at all.
  • Let the browser move focus. showModal() moves focus into the dialog, and close() returns it to the control that opened it. Do not set focus by hand on open or on close, and always close with close(): a dialog hidden by removing its open attribute or by CSS never gives focus back.
  • Keep Escape. The browser closes the dialog on Escape and fires a cancel event first. Only intercept that event to warn about unsaved changes, and then still offer a way to close.
  • Keep a visible close control, and name it. Every modal needs a visible close. Escape is not enough: it is invisible, and it is not available to someone using a pointer alone or a touch screen. The .ds-close holds a <span class="is-sr-only"> that says what it closes, such as “Close figure viewer”. Do not use title or aria-label for this, because page translation tools skip attributes.
  • Confirm the action, do not just ask. The primary button says what it does — “Withdraw”, not “OK” — so a reader who has stopped reading the sentence still knows what they are about to press.

Modifiers

Wide, for a viewer

For a dialog whose content is the point and wants the room — a figure viewer. Not for more text: a wider measure makes a paragraph harder to read, and the default width is already at a comfortable line length.

Note

Where the closedby attribute is unsupported the dialog keeps its close button and Escape, which is the correct thing to fall back to.

Figure 3 — scalar perturbation growth

the figure would fill this stage

Relevant code
<button class="ds-btn" type="button" data-open="demo-wide">Wide — a viewer</button>

<dialog class="ds-modal ds-modal--wide" id="demo-wide" closedby="any" aria-labelledby="demo-wide-title">
  <div class="ds-modal-header">
    <h2 class="ds-modal-title" id="demo-wide-title">Figure 3 — scalar perturbation growth</h2>
    <button type="button" class="ds-close" data-close>
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
      <span class="is-sr-only">Close figure viewer</span>
    </button>
  </div>
  <div class="ds-modal-body">
    <!-- the figure, at full size -->
  </div>
</dialog>
.ds-modal--wide
Added to .ds-modal. The dialog takes nearly the whole viewport, and its height is fixed rather than fitted to the content.
closedby="any"
Light dismiss: a click outside the dialog closes it. Off by default, on purpose. Add it to a dialog the reader is looking through — a figure viewer, an image. Leave it off anything that asks a question, because a stray click outside must not answer it.
.ds-modal-footer
Left out. A viewer has nothing to confirm, so the close control in the header is its only action.

Rules

When a modal is the right answer

Rarely. A modal interrupts: it takes focus, makes the rest of the page inert, and cannot be ignored. That is the whole value and the whole cost.

Reach for a modal whenNot when
The reader must decide before anything else can happen — confirming a withdrawal, agreeing to a data policy. The information is useful but not blocking. That is an alert in the page.
The content is the reader's current task and wants the whole screen — a figure at full size. The content is a lookup they want mid-sentence. That is a popover.
A short form belongs to something on the page and would lose its context on a page of its own. The form is long enough to scroll, or the reader may need to consult the page behind it. Give it a page.
— Announcing something. arXiv does not interrupt readers to tell them things (Brand & vision); the announcement band exists for that and can be ignored.

What the native element gets right for you

These are the five things a modal must do. The reason this component is a <dialog> rather than a <div role="dialog"> is the right-hand column — and hand-built modals fail them in roughly this order.

RequirementNative?Why it is hard by hand
Focus trapYes The browser makes the rest of the page inert — not just untabbable, but unreachable by a screen reader's own navigation, which a JavaScript trap cannot do. A hand-rolled trap has to enumerate every focusable element, and that list is longer than anyone's implementation.
Focus restoreYes close() returns focus to whatever opened it. Forget this and a keyboard user is dropped at the top of the document with no idea where they were.
Escape closes itYes Free, and it fires a cancel event first if you need to intervene — to warn about unsaved changes, say.
Above everythingYes It renders in the top layer with no z-index, so it cannot lose to a stacking context somewhere up the tree. This is what DESIGN-POLICIES means by “modal dialogs use the native <dialog> top layer”.
Scroll lockNo The one gap. The page behind still scrolls, so a reader spinning the wheel loses their place in a document they cannot see. The stylesheet closes it in CSS — html:has(.ds-modal[open]) { overflow: hidden } — so it still needs no JavaScript.

Opening and closing