Skip to content

Docs :: Layout

Everything you need to lay out a page: a grid, a content wrapper, full-page layout scaffolds, and the media queries they respond to.

Grid

File name Source
global.grid.css Github

A lightweight, flexible grid system. It uses the CSS grid property under the hood, but you set up your layouts directly in your HTML. (The implementation is heavily inspired by Raster.)

Syntax

It’s composed of 2 classes: .grid for the container, and .grid_item for each grid item.

The default grid gap is defined as a custom property in theme.default.ui.css and is responsive: 16px (--sm1) on small screens, stepping up to 24px (--sm3) at the --md breakpoint (768px). You can override --grid-column-gap and --grid-row-gap globally in your own theme, or on a specific .grid element. A grid-flush modifier is available if you don’t want any gap.

Because .grid reads its gaps from those two custom properties, overriding a specific grid is just a matter of setting them on the element. Add a class of your own:

.photoWall {
--grid-column-gap: var(--xs2);
--grid-row-gap: var(--xs2);
}
<ul class="grid photoWall" col="2" col-lg="4">
...
</ul>

You define the number of columns of your grid with a custom col attribute and the position/width of your grid items with the custom span attribute.

The span property syntax follows these 5 patterns:

  • row: the grid_item spans a full row.
  • 3: the grid_item starts in the next column and spans 3 columns.
  • 3-5: the grid_item starts in column 3 and ends in column 5.
  • 3+5: the grid_item starts in column 3 and spans 5 columns.
  • 3..: the grid_item starts in column 3 and spans the remainder of the row.

Both the col and span attributes have responsive versions, which use the “mobile first” approach of the --*-n-above media queries. The available tiers are -md, -lg, and -xl (col-md, span-lg, etc.); other suffixes like col-sm or col-xxl don’t exist and are silently ignored. Grids support up to 24 columns. (The file is generated by src/tools/generate.global.grid.cjs; add a tier there if you need one.)

Examples

Different types of spans

9 column grid, with all types of spans.

2
4-5
6..
2-3
5..
2+3
6+2
row
4+2
<div class="grid" col="9">
<div class="grid_item" span="2">2</div>
<div class="grid_item" span="4-5">4-5</div>
<div class="grid_item" span="6..">6..</div>
<div class="grid_item" span="2-3">2-3</div>
<div class="grid_item" span="5..">5..</div>
<div class="grid_item" span="2+3">2+3</div>
<div class="grid_item" span="6+2">6+2</div>
<div class="grid_item" span="row">row</div>
<div class="grid_item" span="4+2">4+2</div>
</div>

Responsive grid

The grid layout follows a “mobile first” approach using --*-n-above media queries.

The following example goes from 1 → 4 → 9 → 12 columns. (Resize window to see it in action.)

1
2
3
4
5
6
7
8
9
10
11
12
<div class="grid" col="1" col-md="4" col-lg="9" col-xl="12">[…]</div>

Wrap

File name Source
global.wrap.css Github

The .wrap global class is a simple content wrapper: it centers your content on the page and adds some left/right padding on small viewports.

The content column comes in three width presets, set with a modifier on the wrapper: .wrap-sm, .wrap-md, and .wrap-lg. A bare .wrap uses the md width (minmax(0, 70ch)). There is also a .wrap_content-fullBleed modifier for children that should extend to the full size of the viewport. See demo.

Syntax

<div class="wrap">
<section class="wrap_content">
<!-- Reading width content (md preset) -->
</section>
</div>
<div class="wrap wrap-lg">
<section class="wrap_content">
<!-- Wider content (up to 900px) -->
</section>
</div>
<div class="wrap">
<section class="wrap_content wrap_content-fullBleed">
<!-- Viewport width content -->
</section>
</div>

Settings

Three responsive paddings and the content widths, set in theme.default.ui.css. Override them in your theme.

Token Value Description
wrap-spacing var(--sm1) default left/right padding
wrap-md-spacing var(--sm3) left/right padding tablet and up
wrap-lg-spacing 0 left/right padding laptop and up
wrap-width-sm minmax(0, 55ch) narrow column (.wrap-sm)
wrap-width-md minmax(0, 70ch) reading column (.wrap-md)
wrap-width-lg minmax(0, 900px) wide column (.wrap-lg)
wrap-width var(--wrap-width-md) width of a bare .wrap

The “tablet and up” and “laptop and up” paddings follow the responsive setup below.

Layouts

File name Source
global.layout.css Github

Page-level scaffolds: the skeleton between your body tag and your content. Every variant extends the same .layout base, an app shell with a header, a stretching content area, and a sticky footer.

Syntax

<body class="layout layout-sidebar">
<header>…</header>
<div class="layout_content">
<aside class="layout_content_aside">…</aside>
<main class="layout_content_main">…</main>
</div>
<footer>…</footer>
</body>

The header and footer are optional: a bare .layout with only .layout_content still gives you a full-height page (that’s what .layout-cover relies on).

The layouts

Each thumbnail links to a full-page demo built from labeled color blocks. Resize the demo window to see the breakpoints, sticky footer, and full-viewport behavior.

Class What it’s for
.layout The base class: header / content / footer with a sticky footer. Required by every variant, usable alone.
.layout-centered A single centered reading column (articles, blog posts, legal pages). Width capped at --layout-content-width.
.layout-sidebar Aside + main from the --md breakpoint, single column below it (aside follows source order).
.layout-sidebar-end Compose with .layout-sidebar to move the aside to the opposite side.
.layout-docs Documentation: nav aside + main + TOC aside (.layout_content_aside-toc). One column below --md with the asides hidden, nav + main from --md, TOC from --lg.
.layout-split Two equal full-height panes (the direct children of .layout_content), stacked below --md. Auth screens, image + content landings.
.layout-cover Full-viewport (100dvh) cover with content centered both ways. Landing, 404, coming-soon pages.

Container queries

Every layout main column (.layout_content_main) is a named size container (container: main / inline-size). You can use it in your container queries:

@container main (width >= …);

Settings

Column widths come from three tokens in theme.default.ui.css. --layout-toc-width accepts any grid track size; the 1fr default lets the TOC resize to whatever space the main column doesn’t use.

Token Value Description
--layout-content-width 70ch main column cap (.layout-centered, .layout-docs)
--layout-aside-width 245px aside column (.layout-sidebar, .layout-docs)
--layout-toc-width 1fr TOC column (.layout-docs)

Override them globally in your theme, or per-layout with a scoped rule. This very site uses .layout-docs and layers its own specifics on top (source): a wider 77ch reading column and sticky side navs.

Prose

File name Source
global.prose.css Github

The global .prose class can be used anywhere you need basic typography for long form text, like articles, blog posts, etc.

It makes spacing between paragraphs, lists, tables, etc. consistent. This spacing can be modified in your theme via the --prose-spacing token.

The .prose class also adds extra styling for ol, ul, and code elements.

Accessibility

File name Source
global.a11y.css Github

Accessibility specific styles, used across the framework and the components:

  • :focus-visible: a unified keyboard focus ring on everything. Customizable in your theme via the --focus-ring-color, --focus-ring-width, and --focus-ring-offset tokens.
  • .a11y-skipLink: skip-to-content link. It stays off-screen until focused.
  • .a11y-srOnly: hides the element visually, but keeps it accessible to screen readers.

Place the skip link first in your <body>, pointing at the id of your main content:

<body class="layout">
<a class="a11y-skipLink" href="#main">Skip to content</a>
<header>…</header>
<main id="main">…</main>
</body>

Animation

File name Source
global.animation.css Github

Basic fade in/out animations with flexible start/end opacity values. Each keyframe only pins one end of the fade; the other end is the element’s own opacity:

Keyframes What it does
fadeFromTransparent fades from 0 to the element’s opacity
fadeFromOpaque fades from 1 to the element’s opacity
fadeToTransparent fades from the element’s opacity to 0
fadeToOpaque fades from the element’s opacity to 1
.element {
opacity: 0.8;
animation: fadeToTransparent 1s forwards;
}

(More info about forwards.)

Media queries

File name Source
settings.media-queries.css Github

All the breakpoints, user preferences, and device capabilities, as named custom media queries you can use anywhere in your CSS.

Viewport size

Dimensions

Token keyword Dimension (px)
xxs 0-240
xs 240-360
sm 360-480
md 480-768
lg 768-1024
xl 1024-1440
xxl 1440-1920

Variations

Using the md dimension as an example:

Token variation Matches
--md-only exact range
--md-n-above range top and above
--md shorthand for --md-n-above
--md-n-below range top and below
--md-phone exact range in portrait only

Examples

Custom media query Value
@media(--md-only) {} (480px <= width < 768px);
@media(--md) {} (width >= 768px);
@media(--md-n-above) {} (width >= 768px);
@media(--md-n-below) {} (width < 768px);
@media(--md-phone) {} (--md-only) and (--portrait);

The -phone variant is available from xxs through lg (there is no xl-phone or xxl-phone).

If you search online for the best approach to responsive design and setting up your breakpoints, you’ll come across the technically true but useless “it depends” answer.

Unless you have a good reason not to, you should use a mobile-first approach. What that means is your design works great on small screens out of the box, without any media queries, and then you add your tweaks for larger sizes.

This is how the CSS of most responsive components should be set up:

.exampleComponent {
/* default mobile */
@media (--lg) {
/* responsive tweaks for desktop */
}
}

Without PostCSS plugins:

.exampleComponent {
/* default mobile */
@media (width >= 1024px) {
/* responsive tweaks for desktop */
}
}

User preferences

These map to the prefers-* and related media features, so you can respect user settings without memorizing the syntax:

Custom media query Matches
--motionOK (prefers-reduced-motion: no-preference)
--motionNotOK (prefers-reduced-motion: reduce)
--opacityOK (prefers-reduced-transparency: no-preference)
--opacityNotOK (prefers-reduced-transparency: reduce)
--useDataOK (prefers-reduced-data: no-preference)
--useDataNotOK (prefers-reduced-data: reduce)
--OSdark (prefers-color-scheme: dark)
--OSlight (prefers-color-scheme: light)
--highContrast (prefers-contrast: more)
--lowContrast (prefers-contrast: less)
--invertedColors (inverted-colors: inverted)
--forcedColors (forced-colors: active)

Device capabilities

Custom media query Matches
--portrait (orientation: portrait)
--landscape (orientation: landscape)
--HDcolor (dynamic-range: high) or (color-gamut: p3)
--touch (hover: none) and (pointer: coarse)
--stylus (hover: none) and (pointer: fine)
--pointer (hover) and (pointer: coarse)
--mouse (hover) and (pointer: fine)

Example

These are used like any other custom media query, and they can be combined with each other or with a dimension token:

.exampleComponent {
@media (--landscape) and (--touch) {
/* a phone or tablet held sideways */
}
}

Without PostCSS plugins, the same thing spelled out:

.exampleComponent {
@media (orientation: landscape) and ((hover: none) and (pointer: coarse)) {
/* a phone or tablet held sideways */
}
}