Navigation

v0.0.4
Site pages

Page

A header, the main region and a footer in a frame as tall as the viewport.

A page is the frame of a document that scrolls as a whole: a header, the main region and a footer. One layout class, page, makes the frame at least as tall as the viewport; utilities do the rest.

Page

<div class="page bg-surface-base fg-text-body text-sm">  <a class="skip" href="#page-main">Skip to content</a>  <header class="navbar">    <a class="navbar-brand" href="#">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M12 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18M12 3a9 9 0 0 0 0 18" />      </svg>Atlas</a>    <nav class="navbar-nav" aria-label="Main">      <a class="navbar-link" href="#">Product</a>      <a class="navbar-link" href="#">Pricing</a>      <a class="navbar-link" href="#">Docs</a>      <a class="navbar-link" href="#" aria-current="page">Changelog</a>    </nav>    <div class="navbar-end">      <button type="button" class="btn btn-sm">Sign in</button>      <button type="button" class="btn btn-quiet btn-square btn-sm navbar-toggle" popovertarget="page-menu" aria-label="Open the menu">        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">          <path d="M4 8h16M4 16h16" />        </svg>      </button>      <div class="menu menu-end" id="page-menu" popover>        <a class="menu-item" href="#">Product</a>        <a class="menu-item" href="#">Pricing</a>        <a class="menu-item" href="#">Docs</a>        <a class="menu-item" href="#">Changelog</a>      </div>    </div>  </header>  <main class="grow(1) w-full max-w(720) mx-auto px-lg py-2xl" id="page-main" tabindex="-1">    <p class="eyebrow">Changelog</p>    <h1 class="heading heading-2xl mt-xs">What shipped this month</h1>    <p class="lead mt-md">Every change to Atlas, newest first. Subscribe to the feed to hear about them as they land.</p>    <div class="flex flex-col gap-xl mt-2xl">      <article class="flex flex-col gap-xs">        <p class="m(0) text-xs fg-text-muted">          <span class="badge badge-sm">4.2</span>          <time class="ml-xs" datetime="2026-10-28">28 October</time>        </p>        <h2 class="heading">Faster cold builds</h2>        <p class="m(0) leading-md">Dependency caches are now restored before the install step, which removes about forty seconds from a cold build. Nothing to configure.</p>      </article>      <article class="flex flex-col gap-xs">        <p class="m(0) text-xs fg-text-muted">          <span class="badge badge-sm">4.1</span>          <time class="ml-xs" datetime="2026-10-14">14 October</time>        </p>        <h2 class="heading">Preview comments</h2>        <p class="m(0) leading-md">Leave a comment on any preview deployment, pinned to the element you clicked. Comments follow the element between deployments.</p>      </article>      <article class="flex flex-col gap-xs">        <p class="m(0) text-xs fg-text-muted">          <span class="badge badge-sm">4.0</span>          <time class="ml-xs" datetime="2026-10-01">1 October</time>        </p>        <h2 class="heading">A new dashboard</h2>        <p class="m(0) leading-md">Projects, domains and usage now live on one page, and every list can be filtered from the keyboard.</p>      </article>    </div>  </main>  <footer class="bt(1) bt-solid b-line-hair">    <div class="flex flex-wrap items-center justify-between gap-sm w-full max-w(720) mx-auto px-lg py-lg text-xs fg-text-muted">      <span>© 2026 Atlas Labs</span>      <nav class="flex gap-lg" aria-label="Footer">        <a class="link link-quiet" href="#">Privacy</a>        <a class="link link-quiet" href="#">Terms</a>        <a class="link link-quiet" href="#">Status</a>      </nav>    </div>  </footer></div>
Scroll the page: the navbar takes its material once content is under it.

#How it is built

  • page on the frame. It is a column with a minimum height of 100dvh. The dynamic viewport unit follows a phone's toolbars as they show and hide, so the frame is never taller than what can be seen and never leaves a gap. This is the one thing here a utility cannot write.
  • grow(1) on the main region. It takes the room that is left, which is what keeps the footer at the foot of a short page.
  • A measure on the content, not on the regions. The header and the footer run the full width. The main region and what is inside the footer share one measure, max-w(720) mx-auto px-lg, so their edges line up; the navbar keeps its own padding. Use a wider measure for an index page and this one for reading.
  • Surfaces come from components. The bar is a navbar, sticky by itself; the hairline over the footer is a border utility.

#A short page

When the content is shorter than the viewport, the main region still takes the room and the footer stays at the foot. Center what little there is with flex items-center justify-center on the main region.

A short page

<div class="page bg-surface-base fg-text-body text-sm">  <a class="skip" href="#page-main-short">Skip to content</a>  <header class="navbar">    <a class="navbar-brand" href="#">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M12 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18M12 3a9 9 0 0 0 0 18" />      </svg>Atlas</a>    <nav class="navbar-nav" aria-label="Main">      <a class="navbar-link" href="#">Product</a>      <a class="navbar-link" href="#">Pricing</a>      <a class="navbar-link" href="#">Docs</a>      <a class="navbar-link" href="#" aria-current="page">Changelog</a>    </nav>    <div class="navbar-end">      <button type="button" class="btn btn-sm">Sign in</button>      <button type="button" class="btn btn-quiet btn-square btn-sm navbar-toggle" popovertarget="page-menu-short" aria-label="Open the menu">        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">          <path d="M4 8h16M4 16h16" />        </svg>      </button>      <div class="menu menu-end" id="page-menu-short" popover>        <a class="menu-item" href="#">Product</a>        <a class="menu-item" href="#">Pricing</a>        <a class="menu-item" href="#">Docs</a>        <a class="menu-item" href="#">Changelog</a>      </div>    </div>  </header>  <main class="grow(1) flex items-center justify-center p-lg" id="page-main-short" tabindex="-1">    <div class="empty">      <div class="empty-media">        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">          <path d="M5 14c0-5 4-10 11-10 0 7-5 11-10 11zM5 14l-2 1 1 3M10 19l-1 2-3-1M14.5 9.5h.01" />        </svg>      </div>      <h1 class="empty-title">Nothing has shipped yet</h1>      <p class="empty-description">The first entry appears here with the first release.</p>      <div class="empty-actions">        <button type="button" class="btn btn-sm">Subscribe to the feed</button>      </div>    </div>  </main>  <footer class="bt(1) bt-solid b-line-hair">    <div class="flex flex-wrap items-center justify-between gap-sm w-full max-w(720) mx-auto px-lg py-lg text-xs fg-text-muted">      <span>© 2026 Atlas Labs</span>      <nav class="flex gap-lg" aria-label="Footer">        <a class="link link-quiet" href="#">Privacy</a>        <a class="link link-quiet" href="#">Terms</a>        <a class="link link-quiet" href="#">Status</a>      </nav>    </div>  </footer></div>

#Accessibility

  • Use the landmarks: <header>, <main> and <footer>, one of each, and a label on every <nav>.
  • The first element of the page is a skip link to the main region, which has tabindex="-1" so focus moves there with the jump. A reader who uses the keyboard passes the header in one press.
  • A sticky header covers the top of the page: set scroll-padding-top on the root to the header's height, so a link to a heading does not land under it, or put flash on the headings links point at, which keeps each of them clear by itself.

#Classes

ClassDescription
pageThe frame: a column at least as tall as the viewport, measured in dvh. It draws nothing

#Source

@master/ui/layouts/page.css
/* * Page: the frame of a document that scrolls as a whole. A header, the main region and a footer stack in it. * The frame is at least as tall as the viewport, measured in dvh, so the toolbars a phone shows and hides * neither leave a gap under the footer nor push it out of reach. Give the main region `grow(1)` and it takes * the room that is left, which keeps the footer at the foot of a short page. * A page draws nothing: its surfaces come from components. */@layer components {  .page {    display: flex;    flex-direction: column;    min-width: 0;    /* vh for engines without dynamic viewport units; dvh replaces it where it is known. */    min-height: 100vh;    min-height: 100dvh;  }}