---
title: "アプリシェル"
description: "ページやアプリの枠組みです。ヘッダー、ナビゲーション、メインコンテンツ、アサイド、フッターを配置し、ウィンドウの幅からそれぞれの見せ方を決め、キーボードでは 1 回の操作でコンテンツに届くようにします。"
url: "https://tanrei.chnotchy.com/ja/components/app-shell/"
last_updated: 2026-10-23T11:50:10.000Z
---

# アプリシェル

> ページやアプリの枠組みです。ヘッダー、ナビゲーション、メインコンテンツ、アサイド、フッターを配置し、ウィンドウの幅からそれぞれの見せ方を決め、キーボードでは 1 回の操作でコンテンツに届くようにします。

## 概要

アプリシェルは、ウィンドウの枠組みです。領域を配置するだけで、その中身は描きません。ヘッダーに置くのは利用者側のアプリバー、ナビゲーションに置くのは利用者側のナビゲーションドロワー、フッターに置くのは利用者側のレイアウトです。シェルが加えるのは、自作の枠組みでは忘れられがちなものです。ランドマーク、ひとそろいのウィンドウ幅の区切り、固定されたヘッダーがリンク先を隠さないための余白、そしてキーボードで枠組みを飛び越える手段です。

枠組みはウィンドウ全体を占めるものなので、このページには動く例がありません。ページの中にシェルを描いても、そのシェルは、ページが置かれたウィンドウのウィンドウクラスになってしまいます。[仕様](https://tanrei.chnotchy.com/ja/components/app-shell/#%E4%BB%95%E6%A7%98)には、フィクスチャをプロパティとともに一覧で示します。テストでは、それぞれを専用のウィンドウに単独で描いています。

Web では、利用者側の領域を `<tanrei-app-shell>` で囲みます。この要素はシャドウルートを持たないので、`<header>` はページのバナーに、ただ 1 つの `<main>` はメインのランドマークになり、スクリプトがなくても、クローラーにも読めます。ほかのプラットフォームはまだ実装していません。

### 使い方

**Web**

```html
<link rel="stylesheet" href="@tanrei/web/app-shell.css" />

<tanrei-app-shell aside-order="before" aside-label="このページの内容">
  <a class="tanrei-link" data-standalone slot="skip-link" href="#main">メインコンテンツへスキップ</a>
  <header slot="header">
    <tanrei-app-bar headline="Tanrei">
      <tanrei-button slot="navigation" data-app-shell-menu variant="text" label="メニュー">
        <span slot="icon">menu</span>
      </tanrei-button>
    </tanrei-app-bar>
  </header>
  <tanrei-navigation-drawer slot="navigation" responsive collapsible label="ドキュメント">
    …
  </tanrei-navigation-drawer>
  <aside slot="aside">…</aside>
  <main id="main" tabindex="-1">…</main>
  <footer slot="footer">…</footer>
</tanrei-app-shell>

<script type="module">
  import '@tanrei/web/components/app-shell';
</script>
```

**React**

```tsx
import { AppBar, AppShell, Button, NavigationDrawer } from '@tanrei/react';
import '@tanrei/web/app-shell.css';

<AppShell asideLabel="このページの内容" onWindowClassChange={(event) => setWindowClass(event.detail.windowClass)}>
  <a className="tanrei-link" data-standalone slot="skip-link" href="#main">メインコンテンツへスキップ</a>
  <header slot="header">
    <AppBar headline="Tanrei">
      <Button slot="navigation" data-app-shell-menu variant="text" label="メニュー"><span slot="icon">menu</span></Button>
    </AppBar>
  </header>
  <NavigationDrawer slot="navigation" responsive collapsible label="ドキュメント">…</NavigationDrawer>
  <main id="main" tabIndex={-1}>{children}</main>
  <aside slot="aside">…</aside>
  <footer slot="footer">…</footer>
</AppShell>
```

`@tanrei/web/app-shell.css` は、スクリプトが動く前に枠組み全体をレイアウトします。そのため、サーバーでレンダリングしたページは、スクリプトが届いてもレイアウトが動かず、スキップリンクは、スクリプトが無効でも働きます。要素自身も、同じスタイルシートをページに加えます。

### 書くもの

領域は、`slot` 属性を持つ子です。メインの領域は、`slot` を持たない、ただ 1 つの `<main>` です。

| 領域 | Web での書き方 | シェルが与えるもの |
| --- | --- | --- |
| スキップリンク | `<a class="tanrei-link" data-standalone slot="skip-link" href="#main">` | キーボードで最初にフォーカスが止まる位置。キーボードフォーカスがある間だけ描かれます。 |
| お知らせ | コールアウトを囲む `<div slot="announcement">` | ヘッダーの上、枠組みの幅いっぱい。 |
| ヘッダー | アプリバーを囲む `<header slot="header">` | 幅いっぱい。ページをスクロールしても上端に残ります。 |
| ナビゲーション | `<tanrei-navigation-drawer slot="navigation" responsive>` | 先頭側の端。3 つの見せ方のいずれかになります。 |
| メイン | `<main id="main" tabindex="-1">` | ガター、幅の上限、スキップリンクの行き先。 |
| アサイド | `<aside slot="aside">`、またはサイドシート | 広いウィンドウでは末尾側の端。それ以外ではメインコンテンツの上か下。 |
| フッター | `<footer slot="footer">` | ページの終わり、ガター、余白。 |
| ナビゲーションバー | `<tanrei-navigation-bar slot="navigation-bar">` | 下端。狭いウィンドウのときだけ。 |

領域は、キーボードでたどる順に書きます。スキップリンク、お知らせ、ヘッダー、ナビゲーション、アサイド（メインコンテンツの前に来る場合）、メイン、アサイド（後に来る場合）、フッター、ナビゲーションバーの順です。スタイルシートは領域を名前で配置し、フォーカスの順序を入れ替えることはありません。書かなかった領域は、場所を取らず、ランドマークも増やしません。

## 仕様

- Category: Containment
- Level: base
- Listing: default
- Page: https://tanrei.chnotchy.com/components/app-shell/
- Spec: https://github.com/chnotchy/tanrei/blob/main/specs/app-shell.spec.json

### Availability

| Design system | Has it | Notes |
| --- | --- | --- |
| Tanrei (`tanrei`) | Yes | — |
| Cupertino (Apple HIG-inspired) (`cupertino`) | Yes | — |
| M3 Expressive-like (Material 3 Expressive-inspired) (`m3-expressive-like`) | Yes | — |
| M3-like (Material 3-inspired) (`m3-like`) | Yes | — |

### Platforms

- Web: the element `<tanrei-app-shell>`, registered by importing `@tanrei/web` (or only its own module, `@tanrei/web/components/app-shell`).
- React: `AppShell` from `@tanrei/react`.

| Platform | Name | Status |
| --- | --- | --- |
| Web | `tanrei-app-shell` | lagging |
| SwiftUI | `TanreiAppShell` | planned |
| Jetpack Compose | `TanreiAppShell` | planned |
| Flutter | `TanreiAppShell` | planned |
| React Native | `AppShell` | planned |

Names a platform gives in its own idiom:

| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- | --- |
| `footerHidden` (prop) | `footerHidden` | `isFooterHidden` | `footerHidden` | `footerHidden` | `footerHidden` |
| `navigationOpen` (prop) | `navigationOpen` | `isNavigationPresented` | `navigationOpen` | `navigationOpen` | `navigationOpen` |
| `asideLabel` (prop) | `asideLabel` | `asideLabel` | `asideLabel` | `asideSemanticLabel` | `asideLabel` |
| `windowClassChange` (event) | `windowclasschange` | `onWindowClassChange` | `onWindowClassChange` | `onWindowClassChanged` | `onWindowClassChange` |
| `navigationOpenChange` (event) | `navigationopenchange` | `isNavigationPresented` | `onNavigationOpenChange` | `onNavigationOpenChanged` | `onNavigationOpenChange` |

Statuses: `planned` — In the inventory. Nothing shippable exists yet. `lagging` — An implementation of an earlier spec exists; it has not caught up with the current one. `alpha` — Implemented with tests. The API may still change. `beta` — Feature complete and at parity. Only fixes are expected. `stable` — Covered by semantic versioning. `deprecated` — Still shipped; scheduled for removal.

### Anatomy

- `container`: The frame itself: the whole window (or the whole page), filled with the product's surface color. It carries no state and takes no input.
- `skip-link` (optional): A Link the product gives ("Skip to content") that the shell keeps out of sight until the keyboard reaches it, then shows in a small raised box at the top start corner of the window. The first thing the keyboard reaches; following it moves focus to the main region.
- `announcement` (optional): A strip across the top of the frame, above the header, for a notice that concerns the whole product (a callout the product places there: an outage, a new version, a page that is not translated).
- `header` (optional): The region across the top that names the product or the screen and holds its search and its actions: the product's app bar. The page's banner landmark.
- `menu-button` (optional): An icon-only Button the product puts in its header and marks as the shell's. The shell shows it only while the navigation has no place in the layout, and it opens the navigation over the content.
- `navigation` (optional): The region at the start edge that holds the product's destinations: a navigation drawer. Beside the main content in a wide window, its column of icons in a narrower one, over the content in a narrow one.
- `main`: The content the page is for, and the page's one main landmark. Inset from the frame by a gutter and, by default, no wider than a readable width.
- `aside` (optional): Content that supports the main content: an outline of the page, details of the selected thing, related links. Beside the main content at the end edge in a wide window, above or below it in a narrower one. A complementary landmark.
- `footer` (optional): The region at the end of the page for what belongs to the whole site: its links, its legal line, its language and theme controls. The page's content-information landmark. Shown while the product fills it and has not hidden it (footerHidden).
- `navigation-bar` (optional): The place at the bottom edge for a navigation bar, shown only in a narrow window, for a product whose three to five top-level destinations stay in reach of the thumb.
- `outline` (optional): The hairline the shell draws between two regions that touch, where the system parts things with a line and the region's own component draws none: above the footer, and at the edge of an aside that is not a side sheet.

### Props

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `scroll` | `enum`: `page`, `main` | `"page"` | — | What scrolls. page: the whole page scrolls in the window, the header stays at the top (headerPosition) and the navigation and the aside stay in view beside the content; the default, because it is what find-in-page, anchors, the back button's return to where the reader was, and a phone browser's collapsing bars are built for. main: the frame is exactly as tall as the window and only the main region scrolls inside it, for an app whose panes scroll apart from each other (a mail client, an editor). |
| `headerPosition` | `enum`: `sticky`, `static` | `"sticky"` | `header-position` | Whether the header stays at the top of the window while the page scrolls (sticky) or scrolls away with it (static: an article that should have the whole window). Ignored while scroll is main, where the header never moves. |
| `mainWidth` | `enum`: `contained`, `full` | `"contained"` | `main-width` | contained: the main region's content is no wider than the shell's readable width and is centred in the room the region has, for pages that are read. full: it takes all the room inside the gutters, for a table, a board, an editor. |
| `asideOrder` | `enum`: `after`, `before` | `"after"` | `aside-order` | Where the aside stands in reading and focus order, and so where it is drawn when it is not beside the main content. after: it follows the main content and is drawn below it (related links, details). before: it precedes the main content and is drawn above it (an outline of the page, which is of use only before the page is read). Beside the main content it is drawn at the end edge either way. |
| `footerHidden` | `boolean` | `false` | `footer-hidden` | Whether the footer is left out of the frame although the product fills it: a screen that is a task rather than a page of the site (a checkout, a sign-in, an editor, a full-window map) keeps the site's footer markup and shows none. While true the footer takes no space, draws no line, is no landmark and has no stop in focus order, exactly as an empty footer region. The footer is shown only while the product fills the footer slot and this is false: footerHidden never makes a footer of an unfilled slot. |
| `separation` | `enum`: `auto`, `divider`, `gap` | `"auto"` | `separation` | How regions that touch are told apart. auto takes the design system's choice through comp.app-shell.separation; divider draws the one hairline; gap draws nothing and leaves the regions' own spaces. It concerns only the edges the shell draws: a component in a region that has an edge of its own (an app bar, a navigation drawer, a side sheet) keeps it. |
| `navigationOpen` | `boolean` | `false` | `navigation-open` | Whether the navigation is presented over the content, while it has no place of its own in the layout (or only its column). The menu button asks for true; closing the navigation, and any change of the window's class, makes it false. Ignored while the navigation stands beside the content. |
| `asideLabel` | `string` | `""` | `aside-label` | Names the aside for assistive technology, in the product's language ("On this page"). Required where the aside is filled and what it holds does not name itself (a side sheet is named by its headline). |

### Events

| Event | Description |
| --- | --- |
| `windowClassChange` | The shell decided the window's class for the first time, or the window's width changed it; carries compact, medium or expanded. A product switches what follows the class and has no setting of its own for it: a search's presentation, a side sheet's variant, the form of an outline. |
| `navigationOpenChange` | The navigation asks to be presented over the content or to be put away: the menu button was activated (true), or the open navigation closed — by its scrim, Escape, the platform's back, a drag, a destination that was activated, or a change of the window's class (false). Carries the value asked for. |

### Slots

| Slot | Description |
| --- | --- |
| `default` | The main region: the page's content. A fixture's content string is plain text. |
| `skip-link` | The skip link: a standalone Link whose destination is the main region, with the product's words ("Skip to content"). More than one may be given ("Skip to navigation"); each is shown while it has focus. |
| `announcement` | The notice above the header: a callout the product places, drawn across the frame's width. |
| `header` | The header: the product's app bar, with its search and its actions, and the menu button in its navigation slot. |
| `navigation` | The navigation: a navigation drawer, which draws each of the three presentations itself. Anything else is placed beside the content where the navigation has a place and is not shown where it has none. |
| `aside` | The aside: the product's supporting content, or a side sheet. |
| `footer` | The footer: the product's own layout of links, a legal line and controls. The shell gives it a place and an inset and no anatomy. |
| `navigation-bar` | A navigation bar for the bottom edge of a narrow window. |

### Item: `menu-button`

The one icon-only Button in the header that opens the navigation: a Button of the text variant and the medium size with the menu glyph and no label, unless the product sets otherwise, which the shell shows and hides and does not restyle.

It is a Button.

Names: Web `tanrei-button`, SwiftUI `TanreiButton`, Jetpack Compose `TanreiButton`, Flutter `TanreiButton`, React Native `Button`.

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `label` | `string` | `""` | `label` | Its accessible name ("Menu"), as the Button's label. Required. |

| Event | Description |
| --- | --- |
| `click` | The button was activated, as the Button's click. The shell answers it itself (APS-7); a product need not. |

| Slot | Description |
| --- | --- |
| `icon` | Its glyph, as the Button's icon slot. A fixture item's content is this glyph. |

### States

`enabled`

### Accessibility

- Role: `none`
- Labeling: The shell has no role and no name. Its regions are the page's landmarks: banner (the header), navigation (named by the label of the navigation drawer or the navigation bar it holds), main (exactly one, unnamed), complementary (the aside, named by asideLabel or by the side sheet's headline) and content information (the footer, while it is shown: an unfilled or hidden footer is no landmark). The skip link is named by its own text and the menu button by its label, both in the product's language: Tanrei ships no words.
- Minimum target size: yes

| Keys | Action |
| --- | --- |
| Tab / Shift+Tab | Moves through the frame in reading order: the skip link, the announcement, the header, the navigation, the main content and the aside in the order asideOrder gives, the footer, the navigation bar. The shell itself is not a stop. |
| Enter, on the skip link | Moves focus to the main region and brings its top into view below the header. |
| Enter or Space, on the menu button | Opens the navigation over the content and moves focus into it; Escape there closes it and returns focus to the menu button (the navigation drawer's own keys). |
| / (a filterable drawer's chord, in a narrow window) | Opens the navigation over the content and moves focus to the drawer's filter field (APS-33). Where the drawer is shown, the drawer answers it. |

- The shell takes no input: the skip link is a Link and the menu button an icon-only Button, and each meets the minimum target by its own spec (LNK-17, BTN-5).
- The skip link is what lets a keyboard pass the header and the navigation in one step (WCAG 2.4.1); the landmarks let a screen reader do the same. Both are needed: a sighted keyboard user has no landmark list.
- Focus is never hidden by the frame (WCAG 2.4.11): the scrolling view keeps the height of the sticky header and of the navigation bar clear when it brings a focused element into view (APS-17).
- Reading order, focus order and the order of the markup are one order in every window class (APS-22), which is why the aside's place is a setting (asideOrder) and not something the layout rearranges by width.
- Reflow (WCAG 1.4.10): at 320 points the frame is one column; the widths that decide the class grow with the text on the web (APS-2), so 200 % text in a 1280 window is laid out as a medium one.
- A second navigation landmark is not an error: a navigation drawer, a navigation bar, breadcrumbs and a pagination each name themselves, and only one of the drawer and the bar is shown at a time unless the product keeps both.
- On SwiftUI, Compose, Flutter and React Native there is no skip link: each platform's assistive technology moves between regions by its own gesture or rotor, and the shell marks the regions for it.

### Behaviour

Requirements in EARS form, each with its stable id.

- **APS-1** THE app shell SHALL fill the window with comp.app-shell.container.color, hand everything it holds comp.app-shell.content.color as the platform's inherited foreground, and place its regions in this order: the announcement across the top; the header across the top below it; then a row that holds the navigation at the start edge, the main region, and the aside at the end edge; the footer across the bottom of the page; and the navigation bar across the bottom edge of the window. A region that holds nothing, and the footer WHILE footerHidden is true (APS-31), SHALL take no space, draw nothing and add no landmark, and the regions beside it SHALL take its room. THE app shell SHALL draw no elevation and no corner of its own.
- **APS-2** THE app shell SHALL give the window one of three classes by its width alone: compact WHILE the window is narrower than comp.app-shell.breakpoint.medium, medium WHILE it is at least that wide and narrower than comp.app-shell.breakpoint.expanded, and expanded WHILE it is at least comp.app-shell.breakpoint.expanded wide. It SHALL derive the class anew whenever the width changes, SHALL never store it, and SHALL NOT read the window's height, the device, the pointer or the orientation for it. WHERE the platform measures a window in a unit that grows with the reader's text size (the web's rem) the two widths SHALL be compared in that unit, so that enlarged text moves the frame to a narrower class before its columns become too narrow to read.
- **APS-3** WHEN the app shell first decides the window's class, and WHEN a change of the window's width changes it, THE app shell SHALL emit windowClassChange exactly once with the class, and SHALL make the present class known to everything it holds through the platform's own channel for inherited values, so that a component inside it can follow the class without measuring the window itself. A change of width inside one class SHALL emit nothing.
- **APS-4** THE app shell SHALL choose how the navigation is presented from the window's class: WHILE the class is expanded, beside the main region (the standard presentation); WHILE it is medium and what the navigation region holds has a collapsed form (a collapsible navigation drawer), as that collapsed form beside the main region, widening over the content WHILE navigationOpen is true (the column presentation); and otherwise with no place in the layout, presented over the content WHILE navigationOpen is true (the modal presentation). A navigation drawer in the region SHALL be drawn in each presentation exactly as a responsive drawer draws it (NVD-53 to NVD-55), and the two SHALL never disagree, because comp.app-shell.breakpoint.medium and comp.app-shell.breakpoint.expanded and the two widths that drawer reads alias the same window classes of the system (sys.breakpoint.medium, sys.breakpoint.expanded). WHILE the presentation is modal and the region holds something that cannot present itself over the content THE app shell SHALL NOT show it, SHALL keep it from focus and from assistive technology, and in debug builds SHALL report that the navigation of a narrow window needs a navigation drawer.
- **APS-5** WHILE the navigation has a place in the layout THE app shell SHALL put it at the start edge of the row, as wide as what it holds draws itself (a navigation drawer's width in its present form) and never wider than the window less sys.target.touch, SHALL give the main region all the room that is left, and SHALL add no padding, fill or edge to it. WHILE scroll is page THE app shell SHALL keep the navigation in view while the page scrolls — from below the header WHILE the header is sticky, else from the top of the window, to the window's bottom edge — and what it holds SHALL scroll inside that height by itself (NVD-61), never the page with it.
- **APS-6** WHILE the navigation's presentation is modal and the navigation region holds something THE app shell SHALL show the menu button where the product put it in the header; WHILE the presentation is standard or column, or the navigation region is empty, THE app shell SHALL NOT show the menu button, SHALL keep it from focus and from assistive technology, and SHALL give its place to what is beside it. THE app shell SHALL NOT restyle the menu button: it SHALL render as the icon-only Button it is, from its own tokens, with its own press, focus ring and minimum target.
- **APS-7** WHEN the menu button is activated THE app shell SHALL emit navigationOpenChange exactly once with true and become open — by setting navigationOpen itself on the web, through its binding where navigationOpen is one, and by the product setting it elsewhere — and what the navigation region holds SHALL present itself over the content as a modal overlay (a navigation drawer as NVD-19 says), taking focus. WHEN the open navigation closes (NVD-20, NVD-21) THE app shell SHALL emit navigationOpenChange exactly once with false, navigationOpen SHALL become false the same way, and focus SHALL return to the menu button. THE app shell SHALL expose the menu button as collapsed or expanded, controlling the navigation. WHEN the window's class changes WHILE navigationOpen is true THE app shell SHALL close the navigation at once and emit navigationOpenChange once with false (NVD-54).
- **APS-8** THE app shell SHALL inset the main region's content from the region's start and end by the gutter of the window's class — comp.app-shell.gutter.compact, comp.app-shell.gutter.medium or comp.app-shell.gutter.expanded — and from its top and bottom by comp.app-shell.main.padding-block. WHILE mainWidth is contained the content SHALL be no wider than comp.app-shell.main.max-width and SHALL be centred in the room the region has inside its gutters; WHILE mainWidth is full it SHALL take all of that room. THE main region SHALL never be wider than the room the row leaves it: content that cannot shrink (a table, a line of code) SHALL scroll inside its own box (PRS-15), and SHALL NOT widen the frame or make the page scroll sideways.
- **APS-9** WHILE the window's class is expanded THE app shell SHALL place the aside beside the main region at the end edge of the row, comp.app-shell.aside.width wide, its content inset by comp.app-shell.aside.padding-inline at the start and the end and by comp.app-shell.main.padding-block at the top and the bottom, so that its first line stands level with the main content's; WHILE scroll is page it SHALL keep the aside in view while the page scrolls, as APS-5 says of the navigation, and the aside SHALL scroll inside that height by itself when its content is taller. WHILE the class is medium or compact THE app shell SHALL place the aside in the main region's column, with the main region's gutters and, WHILE mainWidth is contained, its width and centring: above the main content WHILE asideOrder is before and below it WHILE asideOrder is after, comp.app-shell.aside.gap from it, scrolling with it.
- **APS-10** WHERE the aside holds a component that takes its own width in the layout and draws its own edge (a standard side sheet, SSH-1 and SSH-7) THE app shell SHALL give the aside that component's width in place of comp.app-shell.aside.width, SHALL add no inset to it and SHALL draw no edge beside it; WHERE it holds a component that presents itself over the content (a modal side sheet) the aside SHALL take no space in any class. THE app shell SHALL NOT change the variant of what the aside holds: a product that wants a sheet beside the content in an expanded window and over it in a narrower one sets the sheet's variant from windowClassChange.
- **APS-11** WHILE scroll is page THE app shell SHALL place the footer after the row of the navigation, the main region and the aside, across the frame's whole width, so that it is reached at the end of the page and the navigation and the aside stop staying in view where it begins; WHILE scroll is main THE app shell SHALL keep the footer at the bottom of the frame, below the row and above the navigation bar, always in view. THE app shell SHALL inset the footer's content by the gutter of the window's class (APS-8) at the start and the end and by comp.app-shell.footer.padding-block at the top and the bottom, and SHALL lay out nothing inside it: the footer's columns, links and controls are the product's.
- **APS-31** WHILE footerHidden is true THE app shell SHALL NOT show the footer, whatever the footer slot holds: the footer SHALL take no space, draw no line (APS-18), add no landmark (APS-21) and have no stop in focus order (APS-22), the regions beside it SHALL take its room, and WHILE scroll is page the navigation and the aside SHALL stay in view to the end of the page (APS-5, APS-9). WHILE footerHidden is false THE app shell SHALL show the footer exactly while the product fills the footer slot (APS-1, APS-11): it SHALL NOT draw a footer, or expose a content-information landmark, for a slot the product left unfilled. WHEN footerHidden changes THE app shell SHALL draw the new arrangement at once, without a motion of its own, SHALL keep what the footer slot holds as the product gave it, and IF keyboard focus is in the footer as it is hidden SHALL move focus to the main region.
- **APS-32** THE app shell SHALL draw each region in a stacking order of its own, so that nothing a region holds — whatever level its own stacking order (a z-index) gives it — is drawn over another region: WHILE scroll is page THE app shell SHALL draw the header and the navigation bar over every other region, the footer included, while the page scrolls under them, and SHALL draw the footer over the navigation and the aside. WHILE the skip link has keyboard focus THE app shell SHALL draw it over every region (APS-19). THE app shell SHALL itself be one stacking order within the page, so that no level of its parts competes with what the product draws outside the frame. What stands over the whole frame — a menu, a popover, a tooltip, a snackbar, a dialog, a sheet, a modal drawer — SHALL be drawn above it by the platform's own layer for overlays, never by a level within a region.
- **APS-12** WHILE the announcement region holds something THE app shell SHALL place it across the frame's whole width above the header, with no inset of its own, in every class; WHILE scroll is page the announcement SHALL scroll away with the page and a sticky header SHALL then rest at the top of the window, and WHILE scroll is main it SHALL stay above the header. WHEN what the announcement holds is removed or closes (a callout whose open becomes false) THE app shell SHALL give its room back at once.
- **APS-13** WHILE the window's class is compact and the navigation-bar region holds something THE app shell SHALL keep it at the bottom edge of the window across the frame's whole width, over the bottom system inset as the bar itself lays out (NVB-18), in view while the page scrolls, and SHALL end the page, or the frame, above it so that nothing is covered by it; WHILE the class is medium or expanded THE app shell SHALL NOT show the navigation-bar region, SHALL keep it from focus and from assistive technology, and SHALL give it no space. THE app shell SHALL NOT choose between the bar and the navigation for the product: a product whose bar is its navigation in a compact window gives no menu button.
- **APS-14** WHILE scroll is page THE app shell SHALL be at least as tall as the window and as tall as its content, SHALL let the window's own scrolling view scroll the page, and SHALL make no region a scrolling view except the navigation and the aside where APS-5 and APS-9 keep them in view; so that the platform's find-in-page, a link to a place in the page, the return to where the reader was, and a browser's collapsing bars work as they do on a page without a shell.
- **APS-15** WHILE scroll is main THE app shell SHALL be exactly as tall as the window, less the on-screen keyboard where one covers it, SHALL keep the announcement, the header, the footer and the navigation bar in place, and SHALL make the main region the one scrolling view of the frame, reachable and scrollable from the keyboard, with the aside scrolling inside its own height WHILE it is beside the main region and with the main region WHILE it is above or below it. THE window itself SHALL NOT scroll.
- **APS-16** WHILE scroll is page and headerPosition is sticky THE app shell SHALL keep the header at the top of the window, above the main region, the navigation and the aside, while the page scrolls under it; WHILE headerPosition is static the header SHALL scroll away with the page. THE app shell SHALL NOT fill, raise or edge the header: an app bar in it draws its own scrolled condition from the scrolling view it is connected to (APB-9), which is the window WHILE scroll is page and the main region WHILE scroll is main.
- **APS-17** THE app shell SHALL hold the height of what it keeps over the top of the scrolling view — a sticky header's present height, measured again whenever the header changes size, and nothing WHILE headerPosition is static — and SHALL make it known to the scrolling view and to everything it holds, so that a heading or any other place a link or the address moves the view to (PRS-23), and an element that takes keyboard focus, come to rest below the header and never under it; likewise for the navigation bar's height at the bottom. A heading's own space before it (PRS-6) is added to that distance by the prose, not by the shell.
- **APS-18** WHILE separation is auto THE app shell SHALL part its regions as comp.app-shell.separation names, and WHILE separation is divider or gap as that value says, whatever the token says: divider draws a line comp.app-shell.outline.width thick in comp.app-shell.outline.color along the footer's top edge, along the start edge of an aside that stands beside the main region, and between the main content and an aside that stands above or below it, in the middle of comp.app-shell.aside.gap; gap draws no line and leaves the same spaces. The lines SHALL take no room of their own. THE app shell SHALL draw no line beside a region whose component draws its own edge (APS-10, APS-16) and none around the navigation, the announcement or the navigation bar. WHEN separation names a value the app shell does not define it SHALL be treated as auto.
- **APS-19** WHILE the skip-link region holds a Link THE app shell SHALL make it the first element the keyboard reaches in the frame, before the announcement and the header, SHALL draw nothing of it while it does not have keyboard focus and still keep it in the focus order and exposed to assistive technology, and WHILE it has keyboard focus SHALL draw it comp.app-shell.skip-link.offset from the top and the start edges of the window, inside the system insets, above every region and every sticky part: in a box filled with comp.app-shell.skip-link.container.color and raised by comp.app-shell.skip-link.container.elevation, with corners comp.app-shell.skip-link.container.shape eased by sys.shape.corner-smoothing, padded by comp.app-shell.skip-link.container.padding-inline and comp.app-shell.skip-link.container.padding-block, its text in comp.app-shell.skip-link.label.typescale; the Link SHALL keep its own color, underline, focus ring and states (LNK-1). The box SHALL appear and go at once, without motion, and SHALL move no other part of the frame.
- **APS-20** WHEN the skip link is followed THE app shell SHALL move keyboard focus to the main region itself — not to the first control in it — and bring the region's top into view below the header (APS-17) at once, without a scrolling motion, so that the next Tab reaches the first focusable element of the content and a screen reader starts reading there; the main region SHALL take focus only this way and SHALL NOT be a stop of the Tab order.
- **APS-21** THE app shell SHALL expose each filled region as the landmark it is: the header as the page's banner, the navigation and the navigation bar as what they hold expose (a navigation landmark named by its own label), the main region as the page's one main landmark, the aside as a complementary landmark named by asideLabel (or by what it holds, where that names itself), and the footer as the page's content information; the announcement and the skip link SHALL add no landmark. IF the frame holds no main region, or more than one, or an aside that has no name, THE app shell SHALL still lay out what it holds, and in debug builds SHALL report it.
- **APS-22** THE app shell SHALL NOT be focusable and SHALL move focus through its regions in reading order, the same in every class and in either layout direction: the skip link, the announcement, the header, the navigation, the aside WHILE asideOrder is before, the main region's content, the aside WHILE asideOrder is after, the footer, the navigation bar. A region that is not shown (APS-4, APS-6, APS-13, APS-31) SHALL have no stop in that order. THE app shell SHALL NOT keep focus inside a region that has a place in the layout: only a region presented over the content holds it.
- **APS-23** WHEN the window's class changes THE app shell SHALL draw the new arrangement at once, without a motion of its own; SHALL keep scroll, headerPosition, mainWidth, asideOrder, footerHidden and separation as they are; SHALL keep the reader at the same place in the main content as far as the new arrangement allows; and IF keyboard focus is on something the new arrangement does not show (the menu button, a navigation bar's destination) SHALL move it to the main region, unless the navigation it holds has already moved it (NVD-54).
- **APS-24** WHERE the app shell is rendered before the window's width is known or before its own code runs (a page rendered on a server) THE app shell SHALL already be laid out for the window's class by the platform's layout alone — every region in its place, the regions that the class does not show taking no space, the skip link first and working — SHALL present nothing over the content, SHALL make nothing inert, and SHALL emit no event until the class is decided; its own code SHALL then add only what layout cannot: the events, the menu button's opening of the navigation, and the measured distance of APS-17, without moving anything that was already drawn.
- **APS-25** WHILE the text size is enlarged THE app shell SHALL keep its gutters and insets, let every region grow with its text, and SHALL need no scrolling in two directions at any width down to 320 points: in a compact window the frame is one column, the main region never narrower than the window less two gutters, and only a block that scrolls in its own box (APS-8) scrolls sideways. THE app shell SHALL NOT fix the height of any region.
- **APS-26** WHERE the platform's forced-colors or increased-contrast mode replaces the product's colors THE app shell SHALL let the system's colors fill its frame, SHALL draw the lines of APS-18 in the system's text color WHILE they are drawn, and SHALL draw an edge comp.app-shell.outline.width thick in the system's text color around the skip link's box, because the mode removes the fill and the elevation that set it apart.
- **APS-27** THE app shell SHALL extend its fill under every system inset (status bar, camera cutout, home indicator, window caption) and SHALL keep the main region's, the aside's and the footer's content inside the start, end and bottom insets; the header, the navigation and the navigation bar lay themselves out over the insets at their own edges (APB-10, NVD-25, NVB-18), and the app shell SHALL NOT inset them a second time.
- **APS-28** THE app shell SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the app shell is shown THE app shell SHALL draw itself for the new direction at once, without a motion of its own, keeping the window's class, navigationOpen and the reader's place in the content. WHERE the layout direction is right-to-left the navigation SHALL stand at the right edge and the aside at the left, the skip link's box at the top right, the menu button where the header puts its start, and the lines of APS-18 on the mirrored edges; the order of APS-22 SHALL NOT change, and nothing a region holds SHALL be mirrored by the shell.
- **APS-29** WHEN scroll, headerPosition, mainWidth or asideOrder names a value the app shell does not define THE app shell SHALL render with that prop's default, and in debug builds report the unknown value.
- **APS-30** THE app shell SHALL NOT restyle, resize or re-order what the product puts in a region beyond the place and the insets this spec gives, SHALL NOT decide which page is shown, follow a link, load anything, remember anything beyond its own lifetime (which page, how far a region was scrolled, whether the navigation was collapsed) or set the document's title or language; and SHALL NOT scroll anything with motion: every movement it causes (APS-17, APS-20, APS-23) is at once, whether or not motion is reduced.
- **APS-33** WHILE the navigation region holds a navigation drawer whose filterable is true and whose filterShortcut holds exactly one chord, and the navigation's presentation is modal and navigationOpen is false, WHEN the person presses that chord anywhere in the window — read as SRC-38 reads a chord and with its exceptions (a press something else has taken, a key repeating while held, an input-method composition and, for a chord without mod, ctrl or alt, focus in an element that takes text) — THE app shell SHALL keep the press from doing anything else, open the navigation as an activation of the menu button does (APS-7: navigationOpenChange with true), and ask the drawer to focus its filter (NVD-88), so that focus is in the drawer's filter field once it is open. WHILE the drawer is shown — beside the main region, or as its column — the drawer answers the chord itself (NVD-87) and THE app shell SHALL NOT. THE app shell SHALL bind no other key for the navigation.

### Tokens

As written in the token documents (DTCG): an alias in braces names the token it takes its value from, which each design system resolves. On the web each token is a CSS custom property: `--tanrei` and the name after `tanrei`, joined by hyphens (`tanrei.comp.app-shell.…` → `--tanrei-comp-app-shell-…`).

#### `tanrei.comp.app-shell`

| Token | Type | Value |
| --- | --- | --- |
| `comp.app-shell.container.color` | `color` | `{tanrei.sys.color.surface}` |
| `comp.app-shell.content.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.app-shell.breakpoint.medium` | `dimension` | `{tanrei.sys.breakpoint.medium}` |
| `comp.app-shell.breakpoint.expanded` | `dimension` | `{tanrei.sys.breakpoint.expanded}` |
| `comp.app-shell.gutter.compact` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.app-shell.gutter.medium` | `dimension` | `{tanrei.sys.space.space300}` |
| `comp.app-shell.gutter.expanded` | `dimension` | `{tanrei.sys.space.space400}` |
| `comp.app-shell.main.max-width` | `dimension` | `768px` |
| `comp.app-shell.main.padding-block` | `dimension` | `{tanrei.sys.space.space400}` |
| `comp.app-shell.aside.width` | `dimension` | `240px` |
| `comp.app-shell.aside.padding-inline` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.app-shell.aside.gap` | `dimension` | `{tanrei.sys.space.space300}` |
| `comp.app-shell.footer.padding-block` | `dimension` | `{tanrei.sys.space.space400}` |
| `comp.app-shell.separation` | `number` | `{tanrei.sys.behavior.separation}` |
| `comp.app-shell.outline.color` | `color` | `{tanrei.sys.color.outline-variant}` |
| `comp.app-shell.outline.width` | `dimension` | `{tanrei.sys.border.hairline}` |
| `comp.app-shell.skip-link.container.color` | `color` | `{tanrei.sys.color.surface-container-high}` |
| `comp.app-shell.skip-link.container.elevation` | `shadow` | `{tanrei.sys.elevation.level3}` |
| `comp.app-shell.skip-link.container.shape` | `dimension` | `{tanrei.sys.shape.corner.small}` |
| `comp.app-shell.skip-link.container.padding-inline` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.app-shell.skip-link.container.padding-block` | `dimension` | `{tanrei.sys.space.space150}` |
| `comp.app-shell.skip-link.offset` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.app-shell.skip-link.label.typescale` | `typography` | `{tanrei.sys.typescale.label}` |

## ガイドライン

### 3 つのウィンドウクラス

シェルは、ウィンドウの幅だけをもとに、ウィンドウを 3 つのクラスのいずれかに分類します。

| クラス | 幅 | ナビゲーション | アサイド | メニューボタン | ナビゲーションバー | ガター |
| --- | --- | --- | --- | --- | --- | --- |
| `compact` | 600 未満 | 開いたとき、コンテンツの上に重なる | メインコンテンツの上か下 | 表示 | 表示 | 16 |
| `medium` | 600 から 1023 | ドロワーが `collapsible` ならアイコンの列。そうでなければ compact と同じ | 上か下 | 列がなければ表示 | 非表示 | 24 |
| `expanded` | 1024 以上 | コンテンツの横 | コンテンツの横、末尾側の端 | 非表示 | 非表示 | 32 |

- 幅の境目は、トークン `sys.breakpoint.medium` と `sys.breakpoint.expanded` です。`responsive` を付けたナビゲーションドロワーも同じ 2 つを読むので、枠組みとドロワーは同時に切り替わります。
- Web では、幅を `rem` で比べます。そのため、ページを拡大している読み手には、列が読めないほど狭くなる前に、より狭いクラスの配置が使われます。
- クラスは `windowclasschange` で知らされ、`windowClass` に入っています。検索の見せ方、サイドシートのバリアント、アウトラインの形など、シェルが切り替えないものは、これをもとに利用者側で切り替えます。
- Web では、枠組みの中のすべてが `--tanrei-app-shell-window-class` も継承します。そのため、スタイルシートは、スクリプトなしで、スタイルのコンテナクエリを使ってクラスに合わせられます。

### メニューボタン

ヘッダーにアイコンだけのボタンを 1 つ置き、`data-app-shell-menu` を付けます。シェルは、ナビゲーションがレイアウトの中に場所を持たない間だけ、このボタンを表示します。押すと、ドロワーがコンテンツの上に開き、フォーカスがその中へ移ります。ドロワーを閉じると、フォーカスはボタンに戻ります。ボタンは、ナビゲーションが展開されているかどうかを伝えます。

その状態は `navigationOpen`（`navigation-open`）に入っていて、利用者が変えたときは `navigationopenchange` で知らされます。ウィンドウクラスが変わると、ナビゲーションは閉じます。

### スクロールするもの

| `scroll` | スクロールするもの | 用途 |
| --- | --- | --- |
| `page`（既定） | ウィンドウがページをスクロールします。ヘッダーは上端に固定され、ナビゲーションとアサイドは、コンテンツの横で見えたままです。 | 読むためのページ：ドキュメント、記事、設定。 |
| `main` | 枠組みはウィンドウとちょうど同じ大きさで、メインの領域だけがスクロールします。 | ペインが別々にスクロールするアプリ：メールクライアント、エディター。 |

- ふつうは `page` を使います。ページ内検索、見出しへのリンク、読んでいた位置への復帰、スマートフォンのブラウザーで折りたたまれるバーが、どれもシェルのないページと同じように働きます。
- `headerPosition: static` にすると、ヘッダーはページと一緒にスクロールして見えなくなります。
- Web で `scroll: main` を使うときは、アプリバーの `scroll-target` に、メインの要素の id を指定します。
- Web で `scroll: main` を使うとき、（compact か medium のウィンドウで）メインコンテンツの上か下に描くアサイドは、メインの領域がスクロールしても動きません。いまはまだ一緒にスクロールしません。ページと一緒にスクロールさせたい内容は `<main>` の中に置きます。
- Web では、JavaScript のプロパティは `scrolls` です。どの要素にも、すでに `scroll()` メソッドがあるためです。属性は `scroll` です。

### 見出しへのリンクはヘッダーの下で止まる

シェルは、固定されたヘッダーの高さを測り、ページのスクロールするビューに、その分のパディングを付けます。そのため、リンクで移動した先の見出しも、キーボードフォーカスを受けた要素も、ヘッダーの下に隠れず、ヘッダーのすぐ下で止まります。Web では、同じ距離がルートの `--tanrei-app-shell-scroll-offset` に入るので、テーブルのヘッダー行のような、利用者側で固定しているパーツに使えます。シェルの中では、`--tanrei-prose-scroll-offset` は 0 のままにします。

### 何が何の上に重なるか

- ヘッダーとナビゲーションバーは、その下をスクロールしていくもの（フッターを含む）すべての上に重なります。メインの領域、アサイド、フッターにあるものは、`z-index` をいくつにしても、その上には出ません。どの領域も、中にあるものの重なりをその領域の中で閉じるからです。
- メニュー、ポップオーバー、ツールチップ、スナックバー、ダイアログ、シートは、枠組み全体の上に重なります。Tanrei のものは自分でそこに描かれます（Web ではブラウザーのトップレイヤー）。利用者側で作るオーバーレイも同じように、`popover` か、`showModal()` で開く `<dialog>` で描いてください。大きな `z-index` で持ち上げてはいけません。
- Web では、枠組みそのものが 1 つのスタッキングコンテキストになっています。そのため、枠組みの中の重なりの段が、ページが枠組みの外に描くものと競り合うことはありません。

### メインの領域の幅

- `mainWidth: contained`（既定）は、コンテンツの幅を 768 までにし、使える幅の中央に置きます。読むためのページに使います。
- `mainWidth: full` は、ガターの内側の幅をすべて使います。テーブル、ボード、エディターに使います。
- 幅の広いテーブルや 1 行のコードのように、縮められないコンテンツは、自身のボックスの中でスクロールします。枠組みを広げることはありません。

### アサイド

- expanded のウィンドウでは、アサイドはメインコンテンツの横に 240 の幅で置かれ、ページをスクロールしても見えたままです。
- それより狭いウィンドウでは、メインコンテンツと同じ列に入ります。`asideOrder: before` はメインコンテンツの上に描き、ページのアウトラインに向いています。`after`（既定）は下に描き、関連リンクに向いています。
- `asideOrder` は、読む順序でもあります。Web では、マークアップでも `<aside>` を `<main>` の同じ側に書きます。
- アサイドには、`asideLabel` でプロダクトの言語の名前を付けます。
- アサイドに置いたサイドシートは、自身の幅と縁を保ちます。そのバリアントは、`windowclasschange` をもとに設定します。

### フッター

- フッターは、フッターの領域を埋めているあいだ表示されます。埋めなければ、フッターもランドマークもありません。
- `footerHidden`（`footer-hidden`）は、埋めたフッターを、マークアップから外さずに隠します。サイトのページではなく、作業のための画面に使います。決済、サインイン、エディターなどです。このときフッターは場所を取らず、線を描かず、ランドマークにならず、キーボードのフォーカスも止まりません。ナビゲーションとアサイドは、ページの終わりまで見えたままです。
- フォーカスがフッターの中にあるときに隠すと、フォーカスはメインの領域に移ります。

### 領域の間の線

| `separation` | 描き方 |
| --- | --- |
| `auto`（既定） | デザインシステムが選んだもの。既定のシステムでは線です。 |
| `divider` | フッターの上、コンテンツの横にあるアサイドの先頭側の端、メインコンテンツとその上か下に置かれたアサイドの間に、細い線を描きます。 |
| `gap` | 線は描きません。余白は同じです。 |

線は場所を取りません。ヘッダー、ナビゲーション、お知らせ、ナビゲーションバーの周りには、シェルは線を描きません。これらのコンポーネントは、自身の縁を自分で描くからです。ヘッダーのアプリバーは、位置の変わらない領域の縁として下端の線を静止時から引き、ページがその下をスクロールしても保ちます。コンテンツが下に潜り込んだときだけ表示したいときは、バーの `dividerVisibility` を `scroll` にします。

### 推奨

- どのページにも `<main>` を 1 つ置き、スキップリンクが指す id を付けます。
- スキップリンクの文言は、プロダクトの言語で書きます。Tanrei は言葉を同梱しません。
- ナビゲーションの領域には、`responsive` を付けたナビゲーションドロワーを使います。

### 非推奨

- ページのタイトル、説明、アクションをシェルに入れない。それらは[ページヘッダー](https://tanrei.chnotchy.com/ja/components/page-header/)で、メインの領域の先頭に置きます。
- 記事のフッター（前と次のページ、編集リンク）をフッターの領域に入れない。それはメインコンテンツの終わりに置くものです。フッターの領域は、サイト全体に属するもののためにあります。
- CSS で領域の順序を入れ替えない。マークアップの順序が、フォーカスの順序です。

## アクセシビリティ

### コンポーネントが担うこと

- 中身のある領域は、それぞれのランドマークになります。バナー、ナビゲーション、メイン、補足、コンテンツ情報です。要素自身がランドマークにならない領域には、ロールを付けます。
- スキップリンクは、キーボードで最初にフォーカスが止まる位置です。たどると、フォーカスはメインの領域そのものに移り、その上端がヘッダーのすぐ下に見えます。次に Tab を押すと、コンテンツの最初のコントロールに届きます。
- Tab は、読む順序で領域をたどります。この順序は、どのウィンドウクラスでも、レイアウトの方向が左から右でも右から左でも変わりません。表示されていない領域に、フォーカスが止まる位置はありません。
- リンクで移動した先や、フォーカスを受けた要素が、固定されたヘッダーやナビゲーションバーの下に隠れたままになることはありません。
- ウィンドウクラスが変わったときに、メニューボタンのように表示されなくなったものにフォーカスがあれば、フォーカスはメインの領域に移ります。
- 隠したフッター（`footerHidden`）は、アクセシビリティツリーに現れず、キーボードのフォーカスも止まりません。
- 幅 320 のウィンドウでテキストを 200% にすると、枠組みは 1 列になり、横にスクロールするものはありません。
- 強制カラーモードでは、線とスキップリンクのボックスを、システムのテキストの色で描きます。
- 右から左のレイアウトでは、ナビゲーションが右に、アサイドが左に置かれます。フォーカスの順序は変わりません。

### 利用者側で行うこと

- ページごとに `<main>` をちょうど 1 つ置きます。1 つもない場合や 2 つ以上ある場合は、デバッグビルドで報告します。
- アサイドの名前（`asideLabel`）と、ナビゲーションドロワーとナビゲーションバーの `label` を用意します。1 つのページにナビゲーションのランドマークが 2 つあれば、名前も 2 つ必要です。
- スキップリンクの文言と、メニューボタンのラベルを用意します。
- ページごとに、レベル 1 の見出しを 1 つ、メインの領域に置きます。[ページヘッダー](https://tanrei.chnotchy.com/ja/components/page-header/)のタイトルがそれにあたります。
