Skip to content

Layout

Arranges a page into header, side regions, main content, and footer, with the document as the scroll container. Routing and navigation state stay in the application.

Your first week

Meet your team, set up your accounts, and pick a starter project.

Accounts

Ask your manager for access to the workspace and the design files.

Acme handbook

Code
demo.tsx
import {
  Container,
  Heading,
  Layout,
  Link,
  Page,
  SideNavigation,
  TableOfContents,
  Text,
} from '@earendil-works/elanor-react';

const asidePadding = {padding: 'var(--semantic-dimension-space-padding-medium)'};

export default function LayoutDemo() {
  return (
    // minBlockSize: 0 fits the layout into this demo; omit it on a page.
    <Layout.Root style={{minBlockSize: 0}}>
      <Layout.Header>
        <Container>
          <Link href="#">Acme handbook</Link>
        </Container>
      </Layout.Header>
      <Container render={<Layout.Body />}>
        <Layout.Aside aria-label="Handbook sections" style={asidePadding}>
          <SideNavigation.Root aria-label="Handbook">
            <SideNavigation.List>
              <SideNavigation.Item>
                <SideNavigation.Link href="#" active>
                  Onboarding
                </SideNavigation.Link>
              </SideNavigation.Item>
              <SideNavigation.Item>
                <SideNavigation.Link href="#">Policies</SideNavigation.Link>
              </SideNavigation.Item>
            </SideNavigation.List>
          </SideNavigation.Root>
        </Layout.Aside>
        <Layout.Main>
          <Container
            style={{maxInlineSize: 'var(--semantic-dimension-size-max-width-content-small)'}}
          >
            <Page>
              <Heading level={2} size={4} id="first-week">
                Your first week
              </Heading>
              <Text>Meet your team, set up your accounts, and pick a starter project.</Text>
              <Heading level={3} id="accounts">
                Accounts
              </Heading>
              <Text>Ask your manager for access to the workspace and the design files.</Text>
            </Page>
          </Container>
        </Layout.Main>
        <Layout.Aside aria-label="On this page" style={asidePadding}>
          <TableOfContents.Root>
            <TableOfContents.Item>
              <TableOfContents.Link href="#first-week">Your first week</TableOfContents.Link>
            </TableOfContents.Item>
            <TableOfContents.Item>
              <TableOfContents.Link href="#accounts">Accounts</TableOfContents.Link>
            </TableOfContents.Item>
          </TableOfContents.Root>
        </Layout.Aside>
      </Container>
      <Layout.Footer>
        <Container>
          <Text>Acme handbook</Text>
        </Container>
      </Layout.Footer>
    </Layout.Root>
  );
}

Usage

import {Layout} from '@earendil-works/elanor-react';

<Layout.Root>
  <Layout.Header />
  <Layout.Body>
    <Layout.Aside aria-label="Sections" />
    <Layout.Main />
    <Layout.Aside aria-label="On this page" />
  </Layout.Body>
  <Layout.Footer />
</Layout.Root>;

Use one Main per document. Inside an existing main landmark, use <Layout.Main render={<div />}>. A page without asides doesn’t need Body.

In the demo, the outer Container centers Body and the inner one caps the reading width at semantic.dimension.size.maxWidth.content.small. Asides are semantic.dimension.size.sidebar wide by default.

Application layout

Put sidebars outside the content Grid. Container then centers content in the remaining space, capped at semantic.dimension.size.maxWidth.content.large (1216px).

To use another component as the aside, pass Aside to its render prop instead of wrapping it, so there is one landmark: <Sidebar.Root render={<Layout.Aside aria-label="Workspace" />}>. See Sidebar.

<Layout.Root>
  <Layout.Body>
    <Sidebar.Root render={<Layout.Aside aria-label="Workspace" />}>
      {/* Sidebar regions */}
    </Sidebar.Root>
    <Layout.Main>
      <Container>
        <Page>{/* Page content, such as a Grid */}</Page>
      </Container>
    </Layout.Main>
  </Layout.Body>
</Layout.Root>

Header with top navigation

render merges Top navigation and Header into one header landmark.

<Layout.Root>
  <Layout.Header render={<TopNavigation.Root />}>
    <Container>{/* Identity, navigation, and actions */}</Container>
  </Layout.Header>
  <Layout.Main>
    <Container>{/* Page content */}</Container>
  </Layout.Main>
  <Layout.Footer>
    <Container>{/* Footer content */}</Container>
  </Layout.Footer>
</Layout.Root>

Narrow screens and sticky asides

From semantic.dimension.breakpoint.medium (64rem), Body is a row and each Aside sticks to the top, scrolling within 100dvh. Below it, regions stack in source order; asides are not turned into overlays. For a mobile navigation overlay, use Drawer.

Ancestors with overflow or contain change the sticky scroll container; avoid them. Aside has no padding, so leave room for focus outlines.

To disable stickiness, set position: 'static', maxBlockSize: 'none', and overflow: 'visible' on Aside.

Give the header a fixed height on wide screens, then offset the asides by the same amount:

<Layout.Header className={styles.stickyHeader}>…</Layout.Header>
<Layout.Body>
  <Layout.Aside className={styles.belowHeader}>…</Layout.Aside>
  <Layout.Main>…</Layout.Main>
</Layout.Body>
/* 64rem is semantic.dimension.breakpoint.medium; media queries can't use CSS variables. */
@media (min-width: 64rem) {
  .stickyHeader {
    position: sticky;
    inset-block-start: 0;
    box-sizing: border-box;
    block-size: var(--semantic-dimension-size-x-large);
    z-index: var(--semantic-number-layer-overlay);
    background: var(--semantic-color-background-canvas);
  }

  .belowHeader {
    inset-block-start: var(--semantic-dimension-size-x-large);
    max-block-size: calc(100dvh - var(--semantic-dimension-size-x-large));
  }
}

Keep headers that wrap or grow in normal flow. Set scroll-margin-block-start on anchor targets so the header doesn’t cover them.

API

Layout.Root

The page frame. The document scrolls, not Root.

Renders <div>. Accepts its HTML attributes, ref, and render.

Layout.Header

Optional top region. Not sticky by default.

Renders <header>. Accepts its HTML attributes, ref, and render.

Layout.Body

Holds up to two asides and the main content; a row from the medium breakpoint.

Renders <div>. Accepts its HTML attributes, ref, and render.

Layout.Aside

Side region, sticky and scrollable from the medium breakpoint. Name it with aria-label.

Renders <aside>. Accepts its HTML attributes, ref, and render.

Layout.Main

The main landmark. Fills the remaining width, without a cap.

Renders <main>. Accepts its HTML attributes, ref, and render.

Optional bottom region.

Renders <footer>. Accepts its HTML attributes, ref, and render.