Skip to content
Blocks

Page shell

A skip link, the app's bar, one centred column — the frame worn by every page that is neither the map nor a document.

What it is made of

  • GdSkipLink — first in the document, before the bar
  • GdNavbar width="contained" in #nav — so the bar's edges line up with the column beneath it
  • GdAppShell — the ground, the gutters, the main, and the eyebrow/title/sub header
  • Whatever the page is: GdCards, a wizard step, a table
  • GdSiteFooter — the ink band at the bottom of a marketing page

GdAppShell is scaffolding for a page of CONTROLS — the certify wizard, the tracker, the certifier desk, account. A document gets the docs chassis instead: a reading column, a contents rail, a display line on a bleeding band.

The block

Skip to content
Certify

Lodge a certificate

Building work on Lot 14 on RP80432, Riverton City.

Certificate details

Class 1a dwelling · form 15 and form 16 attached.

Certifier

A1 certification, licence 12048.

A 1280px demo viewport that scrolls sideways — under about 1100 every uncapped variant is the same picture. Change the width and scroll to find the column's other edge. This is the real shell, so the specimen brings its own main and its own h1 into a page that already has both: what documenting a page template on a page costs.
Code
<GdSkipLink />

<GdAppShell width="wide" eyebrow="Certify" title="Lodge a certificate">
  <template #nav>
    <GdNavbar width="contained">…</GdNavbar>
  </template>

  <GdCard>…</GdCard>
</GdAppShell>
<GdSkipLink /> → <a href="#main">Skip to content</a> <GdAppShell> → <main id="main" tabindex="-1">

Rules

Picking a width

  • narrow · 480px — one decision: a sign-in card, an invite. It also sits higher on the page than the others.
  • wide · --gd-size-form (520px) — the form column, and the default: a wizard step, the tracker.
  • reading · --gd-size-reading (700px) — prose: an article, a guide, a policy page. Paired with --gd-text-reading, because widening a column under small type only makes the line longer.
  • portal · 1024px — the desk: two working columns and a table between them.
  • full · --gd-size-container (1200px) — account: the same column the bar above it uses, stated as a gutter rather than a cap.

The reading page

A page of prose with a contents list beside it is the same problem whether it is a documentation page, a terms page or a news article — so it is solved once, in css/reading.css, and both GdAppShell and GdDocsPage consume it. Give either one a contents list and you get the arrangement; give it none and every calculation in it collapses to zero. GdAppShell takes the list as a toc prop, because its pages are prose rather than sections; GdDocsPage reads it off the GdDocsSections in its own slot, so a docs page cannot declare a rail that disagrees with its headings.

  • The reserve is declared once and spent twice. The row gives it to the panel; the header pads by it. Both then centre in the same available width.
  • The padding lands on the element that PAINTS the band. A background covers its own padding, so a padded header still bleeds and only its words move — padding a wrapper shrinks the band itself.
  • The pair absorbs space on its outside edges. margin-inline: auto on the body puts an auto margin between the body and the panel too, which shoves the panel to the frame's edge with a hand's width of nothing beside the text it indexes.
  • The host supplies the column, the sheet supplies the position. Two components each capping a width is how an article once asked for 700px inside a 520px shell and rendered at 520.
  • The panel costs the frame, not the column. On a reading page the shell's main IS the prose column, so it grows by the reserve rather than letting the panel eat the text.

The column

GdAppShell is the container. It declares --gd-app-shell-column on its ROOT — not on <main> — so the bleeding header and the padded main resolve the same number, and alignment is a property of the structure rather than something each page arranges. GdContainer is the same idea for a page that is not using the shell; GdProse owns what goes INSIDE the column and deliberately owns no width of its own.

Everything else

  • Put GdSkipLink before the bar, first in the document.
  • Give whatever the skip link points at a tabindex="-1". GdAppShell does it for #main; anything else you name is your job.
  • Pass width="contained" to the bar in #nav so its edges line up with the column beneath it.
  • A content page takes ground="surface"; an app surface keeps the default. The off-white page ground exists so cards and panels lift off it. A reading page has no cards to lift — it is one column of text — so the off-white gives it nothing and takes contrast from the type. It also costs the header its band: GdPageHeader.is-neutral paints the same --gd-surface-page, so on the default ground the band and the body are the same colour and the header stops reading as a header.
  • ONE COLUMN PER PAGE, and the shell declares it. Nothing inside re-caps a width. A prose page is width="reading" — not a full shell with a 700px cap inside it, which is two columns on one page: the bleeding header aligns to the outer one and the body centres in it, 240px apart. Both halves look reasonable in isolation, which is exactly why the rule is worth stating.
  • Put a GdPageHeader in #header, never the default slot. The default slot is inside <main> and its gutter, so the band stops short of the viewport on both sides. #header is outside it, and the shell hands the header its own column so the words still land on the content's left edge.
  • A PUBLIC page gets #footer; a signed-in work surface does not. A stranger arriving on a landing, news or terms page needs the disclaimer, the legal links and a way out. A marketing footer under a wizard is a second navigation nobody asked for, and the disclaimer belongs beside the verdict rather than at the end of a workflow.
  • Fill the footer once, in the app. The band is the system's and the links are yours — so write one app-level component and render it from every public page. Two pages each filling GdSiteFooter themselves is two different ways out of the same site.
  • Run the footer on the ink surface. Never a hairline over the same white.
  • Put the disclaimer beside the mark at reading size, not in the copyright line.
  • Write a footer column as a nav with a heading — the links are the app's, the caps and rhythm are the system's.
  • Keep links underlined inside prose and undecorated inside a column.
  • Keep GdSiteFooter a direct child of the page root. Nested in an article it stops being the contentinfo landmark.

Behavior & Anatomy

Why the shell is in the system at all

Because a frame that lives in one page's stylesheet is a frame the next page does not get. Classes scoped to a landing page look like something every page already has, right up until a page is built that does not have it — and it ships unstyled, because nothing failed.

Five widths, because there are five jobs

Four cap a column; the fifth states the same column as a gutter instead.

  • narrow · 480px — the one-decision column. It sits higher on the page than the others, because a single card floating in the vertical middle of a tall viewport reads as an error state.
  • wide · --gd-size-form — the token means exactly this, the width a form column is set to, which is why the default is named rather than measured.
  • reading · --gd-size-reading — the prose column, and the one that had to be ADDED rather than carried across. Its absence was a defect, not a gap: a long-form article had no correct width among the interface ones, so it took wide — the 520px form column — and set its own 700px reading column inside. The inner rule was dead. An article that asked for 700 rendered at 520, and a fix that widened only the inner column changed nothing visible, because the outer cap was never the inner column's to change. Prose now gets a prose column at the level that owns it.
  • portal · 1024px — two working columns and a table between them, which stops being usable somewhere under this and stops being readable somewhere over it.
  • full · --gd-size-container — the same column the bar above it uses, so the page's content edges meet the toolbar's.

The skip link, and the half that gets left out

The tabindex is that half. A fragment link moves the reading position in every browser but moves FOCUS only to something focusable, so a skip link pointing at a plain main leaves the keyboard exactly where it was — it has skipped nothing for the person who needed it. Point to somewhere else and whatever it names has to take the same attribute.

It goes before the bar, not after: a skip link that comes after the navigation it skips is only reachable once you have tabbed through it. And it carries no styles of its own — .gd-skip-link is defined in base.css beside the focus ring, because a skip link is a global behaviour and has to work on a page that has not adopted these components yet.

The footer

  • The ink surface is the rule. A marketing page runs two background tones and the footer is where it STOPS. A hairline over the same white says the content merely ran out — which is what the two hand-rolled footers this replaces both were, and by the time they were read side by side they disagreed on the font size, the link ink and the border.
  • The disclaimer is furniture, not fine print. It sits beside the mark at reading size. Shrunk into the copyright line it reads as something we would rather you did not notice.
  • A column is a nav with a heading. The links are the app's business — they are routes — while the caps, the rhythm and the hover ink are the system's, so the footer styles the slotted structure rather than taking a list of links as a prop. Same split, same reason, as the nav links in the bar.
  • Links keep their underline inside prose. In the disclaimer and the copyright line a link that differs from the words around it only by ink is not distinguishable to anyone who cannot see that difference. A column has no prose to be distinguished from, so its links drop the underline.

Navigate

Esc