Skip to content

Responsive layout

Every Concourse page fits the screen width, from a 320 pixel phone to a desktop

Every page in every Concourse app must work on a phone. A page must never scroll horizontally. PRD 0011 records the product requirement.

No horizontal page scrolling

At a viewport width of 320 CSS pixels or more, no part of a page can extend past the right edge of the viewport. This rule applies to every app, every route, and every state of a page, such as an open menu, an error message, or a full table.

Change the layout so that the content fits:

  • Let text wrap. Break long values such as email addresses and URLs with overflow-wrap: anywhere.
  • Change rows into stacked blocks on a narrow screen. A data table becomes a list of records, and each record shows its labels next to its values.
  • Change a grid of columns into fewer columns, or into one column.
  • Give an input or a button width: 100% and min-width: 0 on a narrow screen. Do not give it a fixed minimum width that is wider than the screen.

Do not clip the content to hide the problem. overflow-x: hidden or overflow: hidden on html, body, or a page container is not a fix. The content still extends past the edge, and the user cannot see it.

Allowed exception

A code block, such as a <pre> element or a documentation code sample, can scroll horizontally inside its own box. Wrapping changes the meaning of code and command lines. The box itself must fit the viewport. The page around it must not scroll.

No other element can scroll horizontally. A data table must change its layout instead.

Good example

The MIDOT tool directory shows tool cards in four columns. Below 64rem it shows two columns, and below 36rem it shows one column. Each card keeps its full width, and the page never scrolls sideways.

.tool-grid {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

@media (max-width: 36rem) {
  .tool-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

Bad example

A table in a container with overflow-x-auto scrolls sideways inside the page on a phone. The user must scroll in two directions to read one row. The shared table component worked this way before the check existed.

<div data-slot="table-container" className="relative w-full overflow-x-auto">
  <table className="w-full caption-bottom text-sm" />
</div>

How the rule is checked

Each app's browser tests visit each route at widths of 320 and 375 pixels. The check fails when an element's right edge is past the right edge of the viewport. It also fails when a box with overflow: hidden, clip, auto, or scroll holds content wider than the box, because that content is clipped or scrolls sideways. The check reads element positions. It does not read only the scroll width of the page, because overflow: hidden can hide the overflow from that value. Code blocks, form fields, and visually hidden text are the only elements that the check allows to have content wider than their box.

expectRoutesFitNarrowViewports in @concourse/vitest-config/layout runs the check. Each app has a tests/e2e/responsive-layout.spec.ts that lists its routes. The documentation apps read their pages from /llms.txt, so a new page is checked without a change to the test. In the other apps, add each new route to the list in the same change as the route.

The shared table component stacks its rows below the md breakpoint. Each TableCell needs a label, so that a narrow screen shows the column name above the value.