# 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.

```tsx
// 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

```tsx
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](/components/container.md) 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](/components/sidebar.md).

```tsx
<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](/components/top-navigation.md) and Header into one header landmark.

```tsx
<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](/components/drawer.md).

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.

## Sticky header

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

```tsx
<Layout.Header className={styles.stickyHeader}>…</Layout.Header>
<Layout.Body>
  <Layout.Aside className={styles.belowHeader}>…</Layout.Aside>
  <Layout.Main>…</Layout.Main>
</Layout.Body>
```

```css
/* 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`.

### `Layout.Footer`

Optional bottom region.

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