Modal dialogs

A window that overlays the background page until it is answered or dismissed. There are many ways to build a modal wrong. The design system starts with a native <dialog>, which gets four of the five hard parts right by default. The last part is achieved with css.

The modal dialog

Click either button below to open a real .ds-modal. Modals can be closed with Escape, the close button, or by clicking one of the action buttons.

Test keyboard-tabbing past the last control and scrolling the page behind the modal: neither will work. The modal is appropriately focus-trapped, critical for accessibility. Long content is scrollable within the modal.

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 has 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. It scrolls whenever its content does not fit, at any screen size, including content a script adds after the dialog opens. On a screen too short to hold the header, the footer and some of the body, the whole dialog scrolls instead, so the actions are never cut off.
tabindex="0", role="region", aria-labelledby (added by script)
Not in the markup: the page script adds them while the body overflows and removes them when it fits. A scrolling area with no links or buttons in it cannot be reached from the keyboard in every browser, so a keyboard user could not scroll it. The tab stop fixes that, and the region takes the dialog's title as its name so a screen reader says what it is. Adding it only while the body overflows means a short dialog has no empty tab stop, and a body that grows later, because a script added content or the window got smaller, gets one without anyone changing the markup. The reference script is at the end of this page.

Accessibility essentials

Modifiers

Wide viewer

For a dialog where you need maximum viewing space, like a figure, add class .ds-modal--wide. Don't use the wide modifier for more text only. The default width is set to a comfortable line lenth and increasing the width reduces readability.

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 to use a modal

Rarely. A modal interrupts the user experience, takes focus, makes the rest of the page inert, and cannot be ignored. Use this table to decide:

Reach for a modal whenNot when
The user must decide before anything else can happen, for example confirming a deletion or agreeing to a policy. The information is useful but not blocking, and the reader did not ask for it. The right options are an alert or marginalia note.
Displaying a full-size version of a figure. It would disrupt reading if placed directly on the page at that size. The user needs a quick lookup related to specific content on the page. The right component is a popover.
The user would lose context if the content were placed directly on the page. For example, a short optional form. The form is long enough to scroll, or the reader may need to consult the page behind it for context. The right choice is to build it out as a full page form.
The reader asks to look through a long list or explanation, such as every category a report covers, that would push the main content down if it were on the page. The reader wants to keep the page in view while reading it. The right component is a popover.
— Announcing something. arXiv does not interrupt readers to share news. The right components is the announcement band which can be dismissed or ignored by users and does not interrupt them.

The five requirements of a modal

The reason this component is a <dialog> rather than a <div role="dialog"> are the reasons listed below. Hand-built modals often fail at least one of them.

RequirementNative?What it does
Focus trapYes The browser makes the rest of the page unreachable by a screen reader's own navigation (including keyboard tabbing) which a JavaScript trap cannot do. A hand-built trap must enumerate every focusable element and that list is pretty long.
Focus restoreYes close() returns focus to whatever opened it. If you forget this, a screen reader or keyboard-only user will be dropped at the top of the document again and lose their place.
Escape closes itYes Default behavior, and it fires a cancel event first if you need to warn the user about unsaved changes, etc.
Overlays everythingYes It renders in the top layer with no z-index, so it cannot mistakenly lose its order to a stacking context somewhere up the tree.
Scroll lockNo The one gap that <dialog> does not cover by default. The stylesheet resolves it with html:has(.ds-modal[open]) { overflow: hidden }. Still JavaScript-free.

Opening and closing

  • Never open one unprompted. A modal is a response to something the reader did. One that appears on load is an interruption they did not ask for, and arXiv does not interrupt readers to tell them things.
  • Never nest them. A second modal over the first leaves no way back except forward. If a modal needs a modal, it needed a page.
  • Nothing animates. An entrance animation also animates the moment the page goes inert, and there is nothing here for prefers-reduced-motion to switch off when nothing moves.