AppShell

The page frame: header, optional subnav, main and footer.

Overview

Every page of an app sits in one AppShell; it is the only place the page canvas, the font and the header live.

Brand

Do
import { Avatar, AppShellBrand } from "@hackcove/ui";

<AppShellBrand href="#" logo={<Avatar name="Quotes" size="sm" />} label="Quotes" collapse />
AppShellBrand makes the logo and the label one link, so the whole brand is the way home; collapse leaves only the logo below sm.
Don't
import { Avatar, Cluster, TextLink } from "@hackcove/ui";

<Cluster gap="xs">
  <a href="#">
    <Avatar name="Quotes" size="sm" />
  </a>
  <TextLink href="#">Quotes</TextLink>
</Cluster>
A logo and a name as two separate links are two targets, and the logo link has no name. Put both in AppShellBrand.

Router brand

Do
import { Avatar, AppShellBrand } from "@hackcove/ui";

<AppShellBrand
  logo={<Avatar name="Quotes" size="sm" />}
  label="Quotes"
  render={(props) => <a {...props} href="#home" />}
/>
With a router, render draws the app's own link and AppShellBrand adds the logo, the label and the focus ring (here a stand-in <a>).

Usage

  • Fill the slots and write no wrapper: brand, nav (a NavTabs), controls (a picker, a ColourSchemeToggle), then an optional subnav row and footer.
  • brand is an AppShellBrand: logo and label in one link. collapse leaves only the logo below sm.
  • With a router, give AppShellBrand a render that draws the app's own <Link> instead of an href.
  • Header, main and footer each sit in a Container, so a page never sets its own width or padding. Do not add a Container inside the shell.
  • bleed drops main's Container for a layout that needs the whole width (FullBleedLayout); SidebarLayout needs no bleed.
  • hideChromeInPrint hides the header and footer and main's padding on paper; screen is unchanged.
  • header is for a node no other slot covers. Prefer brand, nav and controls.
  • Put appShellRootClassFor(scheme) on the app's own <html>. AppShell never touches <html>.
  • A theme sets the canvas colour and the font; the app writes no page background or font rule.

Accessibility

  • The header is a <header> (the banner landmark), main is <main> and the footer is <footer>.
  • AppShellBrand is one link named by its label; the logo is aria-hidden. With collapse the label is visually hidden below sm and still names the link.
  • A header row that is wider than the screen (a long nav) scrolls sideways and never wraps; a link that takes focus scrolls into view, and the side with more to reach fades out.
  • Give nav and subnav a NavTabs so each is a labelled <nav>.

API

AppShell

The page frame: it paints the page canvas and sets the type face and ink, so an app writes no page-background or font rule of its own (a theme remaps the canvas and the font stacks). A sticky one-row header, an optional second subnav row, the main area and an optional footer. Header, main and footer each sit in a Container, so pages never set their own width or padding. bleed drops main's Container (the header keeps its own), for a layout that needs the whole width (FullBleedLayout). Main is a flex column. SidebarLayout needs no bleed: its rail and canvas share the header's Container. The header row is laid out by its slots, left to right: brand, nav, then header (anything the slots do not cover), then controls, pushed to the end. An app writes no wrapper or styling for any of them.


bleed

Main without a Container, for a full-bleed layout.

Type
boolean
Default
false

brand

The brand: a logo and a name, in the header's brand type, with the gap between them. AppShellBrand makes both one link.

Type
ReactNode

controls

The end of the row: a picker, a theme toggle. Gap-2, vertically centred; no child shrinks, and a full-width control (a Select at its default) sizes to its content.

Type
ReactNode

footer
Type
ReactNode

header

The escape hatch: a free node in the header row, after brand and nav.

Type
ReactNode

hideChromeInPrint

Print only the page: the header and footer are hidden, and main's Container drops its padding (the page margin is then @page's). Off by default, so a print is unchanged unless an app asks.

Type
boolean
Default
false

nav

The main navigation, e.g. NavTabs (it is already a <nav>).

Type
ReactNode

subnav

A second fixed-height row under the header, e.g. NavTabs.

Type
ReactNode

AppShellBrand

The brand slot as one link: a logo and a label inside the same <a>, so the whole brand is the way home. The label names the link; the logo is decoration and hidden from assistive tech. collapse shows only the logo below sm, the label staying in the accessible name. With render it draws the app's own router <Link> (render={(p) => <Link to="/" {...p} />}).


collapse

Below sm only the logo shows; the label is visually hidden and still names the link. Needs a logo.

Type
boolean
Default
false

href
Type
string

label required

The name beside the logo. It is the link's accessible name.

Type
ReactNode

logo

The mark before the label, e.g. an Avatar or an <img>.

Type
ReactNode

render
Type
LinkRender

Functions

  • appShellRootClass The class for the app's own <html> that always reserves the scrollbar, so centred content does not shift between a short page and a long one.
  • appShellRootClassFor Everything the app's <html> takes from the scheme the server knows: that scheme's class and appShellRootClass, joined.