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%andmin-width: 0on 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.