Skip to content
Foundations

Layout, breakpoints & layering

The four window classes, the fixed widths, and the order things stack in.

Breakpoints

ClassTokenWhat survives
Compact0–599--gd-bp-compactthe panel is a bottom sheet and the map is the page
Medium600–839--gd-bp-mediumthe panel docks as a rail
Expanded840–1199--gd-bp-expandedpanel plus map plus chrome, all at once
Large1200–1599--gd-bp-largethe container has stopped growing; margins take the surplus
--gd-bp-compact600pxbelow this the panel is a bottom sheet and the map is the page
--gd-bp-medium840pxthe panel docks as a rail
--gd-bp-expanded1200pxpanel plus map plus chrome, all at once
--gd-bp-large1600pxthe container stops growing; margins take the surplus

The page axis

A page is made of bands — things that paint edge to edge and hold their words inside a narrower measure. The navigation bar is one. So is GdPageHeader, every GdContainer, the footer, and each of the three shells. They are not each other's children, so nothing makes them agree by nesting. Two custom properties do:

  • --gd-page-column — how wide the content column may grow
  • --gd-page-gutter — the space between that column and the band's edge

Every band writes the same three declarations, which is the entire contract:

Riverton City

Lot 4, Jarrah Street

What the scheme allows on this parcel.

The paragraph starts where the title starts, because both read the same two values.

A header and a body, on one axisReal GdPageHeader and GdContainer. The specimen narrows the axis so both fit — one declaration, and the title lands on the same pixel as the paragraph. Nothing here sets a width.
Code
/* every band, identically */
width: 100%;
max-width: calc(var(--gd-page-column) + 2 * var(--gd-page-gutter));
margin-inline: auto;
padding-inline: var(--gd-page-gutter);

/* a reading page, once, on its root */
.page {
  --gd-page-column: var(--gd-size-measure);
}

A page declares the axis once and everything inside follows. A reading page sets --gd-page-column: var(--gd-size-measure) on its root, and its header, its prose and its figures all share one left edge. This is inheritance doing the work — which is why it is custom properties and not props, since a prop would have to be handed to every band separately.

GdContainer narrow re-declares the same variable rather than overriding a width, so anything inside it narrows too instead of being left behind at the wide column.

Containers

GRIDD puts the same components in wildly different amounts of space. A GdMetricCard sits four-across in the 400px parcel panel and also alone in a 1200px document. A GdListRow is a report line in a rail and a settings row on an account page. At 1440px the panel is still 400px wide — so every @media query in the system was telling everything inside it that it had a desktop's worth of room.

GdContainer — the document column
GdContainer narrow — the reading measure
Two live GdContainers. The outer is the document column — it caps at --gd-size-container (1200px) and centres, so above that width the viewport keeps growing and it does not. narrow caps at the reading measure instead, for prose.
Code
<GdContainer>… the document column …</GdContainer>

<!-- capped at the reading measure instead -->
<GdContainer narrow>… prose …</GdContainer>

<!-- it takes the element it should be -->
<GdContainer as="main">…</GdContainer>

It is also the thing that DECLARES the container, not just a width: container: gd-container / inline-size lives on it, which is what lets everything inside ask @container gd-container about the room it actually has. A page column that caps is precisely the case a media query gets wrong — a component here has the same space at 1440px as at 2560px, and only a container query can say so.

Write the roomy state as the default

An unsatisfied container query is never true — including when there is no container above the component at all. So the rules outside the query must be the COMFORTABLE form, and the query may only ever tighten. Get this backwards and a component with no container renders permanently cramped, in the one situation where it has the most space.

Which surfaces are containers

A component asking @container gd-container has to be inside one. These are the surfaces that declare it; anything nested in them can ask.

GdSidePanelthe parcel panel — 400px wide whatever the screen is doing
GdDrawera panel that arrived from an edge; same room, same rules
GdCardthree cards across a wide page are each a third of it
GdGridItema cell is span/12 of the grid, which no viewport can tell a child
GdContainerthe page column CAPS at 1200px — above that the viewport grows and this does not
GdDocsSpecimenso a specimen shows the component at the width the frame really gives it
--gd-container-tight220pxbelow this nothing may sit side by side — a metric's value and its unit stack
--gd-container-narrow360pxthe parcel panel's inner width (--gd-size-panel less its padding): THE reference width for anything panel-bound
--gd-container-wide560pxabove this a component may spread — two columns, trailing content on the title's line
--gd-container-full800pxa document-width column; a component here has as much room as it will ever get

Fixed widths

--gd-size-gutter24pxthe grid gutter — constant at every breakpoint
--gd-size-margin-compact16pxpage margin below 600px
--gd-size-margin-expanded24pxpage margin at 600px and above
--gd-size-container1200pxthe document and marketing column
--gd-size-measure660pxthe interface-prose column — docs paragraphs, ledes, captions
--gd-size-reading700pxthe ARTICLE column — pair with --gd-text-reading
--gd-size-form520pxforms are single-column at this width
--gd-size-panel400pxthe parcel panel
--gd-size-nav200pxthe docs and console side nav. The design allows 184–200; the wide end because a tree level indents 26px and the labels must survive it
--gd-size-toc170pxthe 'on this page' contents rail
--gd-size-rail88pxthe icon nav rail (`GdNavRail`). Wide enough that a 48px target sits in it with air either side, and that a one-word label clears the item without wrapping — a wrapped label makes every item in the column a different height
--gd-size-card-min260pxa card grid drops a column rather than squeezing a card below this
--gd-size-tap44pxthe minimum touch target

Layering

--gd-z-base0the page
--gd-z-map-veil9a dimming veil over the basemap — UNDER the chips it sits behind
--gd-z-map-chrome10hover chips, scale bars — things pinned over the map
--gd-z-map-pill12a floating control pill (layers, filters) — over the chrome, over a docked panel's shadow
--gd-z-map-fab14the one floating action button, above every other map control
--gd-z-panel20the parcel panel and any docked rail
--gd-z-sheet25a NON-MODAL bottom sheet (the report on a phone) — above the panel, below the scrim, because it can be read with the map still live behind it
--gd-z-sticky30a sticky header or toolbar
--gd-z-popover40dropdowns, menus, tooltips
--gd-z-scrim50the dimmer behind a modal
--gd-z-modal60dialogs and sheets
--gd-z-toast70toasts — above the modal, never over its primary action
--gd-z-skip80the skip link beats everything

Rules

  • Cap reading text at 68 characters. Docs and articles cap the column at --gd-size-measure (660px) regardless of viewport. This page is doing it.
  • Let a card grid drop a column rather than squeeze. --gd-size-card-min is 260px and it is a floor.
  • Use full-bleed for the map and the ink CTA band only. Every other section keeps its margins.
  • Keep forms single-column at --gd-size-form (520px). Two fields share a row only when they are one fact — a number and its unit, a month and a year.
  • Never make a touch target smaller than --gd-size-tap (44px). Give it back with a pseudo-element if the visual size has to stay small.
  • Open only one layer above popover at a time. A modal closes any popover.
  • Never let a toast cover the primary action of the modal beneath it, even though --gd-z-toast is above --gd-z-modal.
  • Do not give a dialog a z-index. GdDialog is in the browser's top layer already.

Behavior & Anatomy

Why layout is described by how much map survives

GRIDD is a map app first, so the breakpoints are named for what is still on screen rather than for a column count. The 12-column grid governs documents, marketing, news and the console; the app itself ignores columns because it is a map plus one panel.

The boundaries sit on Material 3's window size class boundaries because the app's own thresholds already sat there. Gutters are a constant 24px at every size — a grid whose gutters breathe with the viewport reflows every card at every width.

Why the map band gets four rungs

Everything else in the system gets one. The map is the one surface with several kinds of chrome floating over each other, and the orderings are recorded bug fixes rather than taste: a veil must sit UNDER the hover chip it dims behind, and a floating pill must sit OVER a docked panel's shadow or it clips. Four rungs is what those two fixes cost.

Why only one layer above popover

A modal closes any popover, and a toast never covers the primary action of the modal beneath it. Two independently dismissible layers over a modal is a state nobody can describe, and the escape key can only mean one thing.

The non-modal bottom sheet — the report on a phone — sits above the panel and below the scrim on purpose, because it can be read with the map still live behind it.

Navigate

Esc