跳到主要内容
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
开始使用WebReactSwiftUIJetpack ComposeFlutterReact Native预设你自己的设计系统与 AI 代理一起使用 Tanrei品牌设计令牌设计系统与定制Theme Studio颜色字体排印图标形状层级动效交互状态行为布局无障碍文字颜色令牌字阶令牌形状令牌层级令牌动效令牌状态令牌间距与度量令牌组件按钮主操作按钮分段按钮按钮组带菜单的按钮工具栏复制按钮复选框单选按钮开关纸片(Chip)菜单滑块滑块字段日历DateTimePicker字段组单选组复选框组下拉选择框下拉选择字段菜单栏文件上传文件选择器 进阶放置区文本输入框 进阶文本字段纸片字段字段组合框 进阶组合框字段数字输入框 进阶数字字段颜色输入框 进阶颜色字段一次性验证码输入框 进阶一次性验证码字段嵌入式文本输入框 进阶聊天输入栏卡片分隔线模态层 进阶对话框列表列表行底部面板侧边面板轮播代码块表格弹出框步骤折叠面板描述列表图示应用外壳页头树视图时间线图片预览调整手柄可调整大小的框滚动区域徽标进度指示器计量条数字滚动器加载指示器消息条(Snackbar)工具提示键盘快捷键标签(Tag)行内代码提示块(Callout)头像空状态标签组(Tag group)骨架屏文件项色块跑马灯文本标签页标签页视图应用栏导航栏导航抽屉搜索链接分页上一页/下一页链接步骤条面包屑目录链接卡片FAB 菜单状态Tanrei 是如何构建的参与贡献许可与致谢
首页组件应用外壳

应用外壳

为页面或应用搭起框架。它安放页眉、导航、主内容、侧栏和页脚,根据窗口的宽度决定各自的呈现方式,并让键盘一步就能到达内容。

content_copy以 Markdown 格式查看
类别
容器
令牌
comp.app-shell
层级
基础

概览

Web
tanrei-app-shell
schedule
SwiftUI
TanreiAppShell
radio_button_unchecked
Jetpack Compose
TanreiAppShell
radio_button_unchecked
Flutter
TanreiAppShell
radio_button_unchecked
React Native
AppShell
radio_button_unchecked

生成自 docs/status/status.json, specs/app-shell.spec.json

应用外壳是窗口的框架。它只安放各个区域,不绘制其中的任何内容:页眉是你的应用栏,导航是你的导航抽屉,页脚是你自己的布局。外壳补上的,是手工搭建的框架容易遗漏的东西:地标、统一的一组窗口宽度、粘性页眉在链接目标上方占去的空间,以及让键盘越过框架的途径。

框架就是整个窗口,所以本页没有它的实时示例:画在页面里的外壳,采用的会是那个页面的窗口类别。规格下的测试样例连同各自的属性一并列出;在测试中,每一个都单独画在它自己的窗口里。

在 Web 上,它是包在你自己的各个区域外面的 <tanrei-app-shell>。这个元素没有 shadow root,所以 <header> 就是页面的横幅(banner),唯一的 <main> 就是页面的主内容地标,不运行脚本时和爬虫都能读到。其他平台尚未实现。

用法

<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>
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 属性(attribute)的子元素。主内容区域是唯一的那个 <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> 起始边,采用三种呈现方式之一。
主内容 <main id="main" tabindex="-1"> 两侧边距、宽度上限,以及跳过链接的目的地。
侧栏 <aside slot="aside">,或者一个侧边面板 宽窗口中位于末尾边。否则位于主内容的上方或下方。
页脚 <footer slot="footer"> 页面的末尾,以及两侧边距和间距。
导航栏 <tanrei-navigation-bar slot="navigation-bar"> 底边,只在窄窗口中出现。

请按键盘经过的顺序书写各个区域:跳过链接、公告、页眉、导航、位于主内容之前的侧栏、主内容、位于主内容之后的侧栏、页脚、导航栏。样式表按名称安放它们,绝不改变焦点的顺序。你省略的区域不占空间,也不增加地标。

规格

构成

部件 说明
1 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.
2 skip-link · 可选 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.
3 announcement · 可选 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).
4 header · 可选 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.
5 menu-button · 可选 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.
6 navigation · 可选 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.
7 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.
8 aside · 可选 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.
9 footer · 可选 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).
10 navigation-bar · 可选 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.
11 outline · 可选 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.

属性

属性 类型 默认值 说明
scroll 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 sticky | static "sticky" 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 contained | full "contained" 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 after | before "after" 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 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 auto | divider | gap "auto" 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 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 "" 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).

插槽

插槽 说明
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.

事件

事件 说明
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.

条目

该组件重复排列的条目种类。测试样例按顺序列出条目;各平台以各自的惯用方式为每个种类命名。

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.

属性 类型 默认值 说明
label string "" Its accessible name ("Menu"), as the Button's label. Required.
插槽 说明
icon Its glyph, as the Button's icon slot. A fixture item's content is this glyph.
事件 说明
click The button was activated, as the Button's click. The shell answers it itself (APS-7); a product need not.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-button TanreiButton TanreiButton TanreiButton Button
label · 属性 label label contentDescription semanticLabel accessibilityLabel
click · 事件 click action onClick onPressed onPress
备注 An icon-only <tanrei-button> anywhere inside the header region, marked with the data-app-shell-menu attribute: usually the app bar's slot="navigation" child. The glyph is its content. An icon-only TanreiButton in the header, marked .tanreiAppShellMenuButton(); the shell reads the mark from the environment and hides the button outside the modal presentation. An icon-only TanreiButton in the header with Modifier.tanreiAppShellMenuButton(); the shell draws it only in the modal presentation. An icon-only TanreiButton wrapped in TanreiAppShellMenuButton, placed in the header; the wrapper builds nothing outside the modal presentation. An icon-only Button element passed as menuButton; the shell hands it to the header through context (useAppShellMenuButton()) and renders it only in the modal presentation.

状态

行为

以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。

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.

各平台名称对照

对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-app-shell TanreiAppShell TanreiAppShell TanreiAppShell AppShell
scroll · 属性 scroll scroll scroll scroll scroll
headerPosition · 属性 headerPosition headerPosition headerPosition headerPosition headerPosition
mainWidth · 属性 mainWidth mainWidth mainWidth mainWidth mainWidth
asideOrder · 属性 asideOrder asideOrder asideOrder asideOrder asideOrder
footerHidden · 属性 footerHidden isFooterHidden footerHidden footerHidden footerHidden
separation · 属性 separation separation separation separation separation
navigationOpen · 属性 navigationOpen isNavigationPresented navigationOpen navigationOpen navigationOpen
asideLabel · 属性 asideLabel asideLabel asideLabel asideSemanticLabel asideLabel
windowClassChange · 事件 windowclasschange onWindowClassChange onWindowClassChange onWindowClassChanged onWindowClassChange
navigationOpenChange · 事件 navigationopenchange isNavigationPresented onNavigationOpenChange onNavigationOpenChanged onNavigationOpenChange
备注 An element and a stylesheet: <tanrei-app-shell> attaches no shadow root, and the package's @tanrei/web/app-shell.css lays out its light-DOM children from the element's name, its attributes and each child's slot attribute alone, so a page rendered on a server is laid out before the element is defined and with scripts off (APS-24). The regions are the product's own semantic elements: <a class="tanrei-link" data-standalone slot="skip-link" href="#main">, <div slot="announcement">, <header slot="header"> around a <tanrei-app-bar>, <tanrei-navigation-drawer slot="navigation" responsive>, the one <main id="main" tabindex="-1"> child without a slot (the default region), <aside slot="aside" aria-label="…">, <footer slot="footer"> and <tanrei-navigation-bar slot="navigation-bar">, written in the order of APS-22; the stylesheet places them on a grid by name and never reorders focus. Attributes: scroll, header-position, main-width, aside-order, footer-hidden, separation, navigation-open, aside-label. The window's class is chosen by @media conditions in rem; a custom property cannot stand in a media condition, so the generator writes the two widths into the stylesheet from the active system's tokens (one stylesheet per system that changes them). The menu button is hidden by the same conditions, with :has(> [slot="navigation"][collapsible]) telling the column presentation from the modal one. Sticky parts are position: sticky; the frame and each region are isolation: isolate (APS-32), the footer is z-index: 1, the header and the navigation bar 2 and the focused skip link 3 — levels within the frame, which no z-index inside a region reaches; the navigation and the aside are align-self: start with a maximum block size of the dynamic viewport height less the scroll offset and overflow: auto with overscroll-behavior: contain. scroll="main" makes the host 100dvh high and the <main> the scroller. It does not yet follow the part of APS-15 that has an aside above or below the main region scroll with it: a stylesheet cannot make the <main> and its sibling <aside slot="aside"> one scrolling view, and making the frame itself the scroller in that arrangement would take the scrolling view away from the <main> that the app bar's scroll-target (APS-16) and the skip link (APS-17, APS-20) name, and would need the announcement's and the navigation bar's heights before the element is defined (APS-24). Until it does, a product that uses scroll="main" and wants the aside to scroll with the content in a compact or medium window writes that content inside the <main> for those classes. The stylesheet defines --tanrei-app-shell-scroll-offset on the document's root (until the element is defined: the small app bar's height while the header is sticky, else 0) and sets the root's scroll-padding-block-start from it — or the main element's while scroll is main; once defined, the element measures the header with a ResizeObserver and rewrites the property. The product's own sticky parts (a table's header row) read the same property. --tanrei-prose-scroll-offset stays 0 inside a shell: the scroll padding already clears the header, and prose adds only a heading's own space. The upgraded element adds: the read-only windowClass property, reflected as the data-window-class state attribute, and windowclasschange with detail.windowClass, from two matchMedia listeners (no resize listener and no frame loop); the menu button's click, which sets navigationOpen and the open of a <tanrei-navigation-drawer> in the navigation region, follows that drawer's close, and writes aria-expanded and aria-controls through the Button's element properties; role and aria-label on a region whose element does not already give the landmark (a <tanrei-app-bar slot="header"> with no <header> around it; an aside without a name takes aside-label); tabindex="-1" on a <main> that lacks it; and the debug reports of APS-4, APS-21 and APS-29. It listens to nothing while no region needs it. With scroll="main" the product names the main element's id in the app bar's scroll-target. footer-hidden gives the footer region display: none from the stylesheet (with !important, so a product's own display on its <footer> cannot bring it back): it then has no box, no landmark and no focus stop, its measured height is 0, and the element moves focus that was inside it to the main region. The skip link is hidden with a clip, never display: none, and shown on :focus-visible; under forced-colors: active its box gains a CanvasText border. Insets are env(safe-area-inset-*). A filterable drawer’s chord (APS-33): the element holds one keydown listener on the window while the navigation’s presentation is modal, navigationOpen is false and the drawer in the navigation region is filterable with exactly one chord in filterShortcut (read as the drawer reads it); the chord opens the navigation as the menu button does and then calls the drawer’s focusFilter(). The React wrapper renders the same light DOM on a server. TanreiAppShell(scroll:, headerPosition:, mainWidth:, asideOrder:, isFooterHidden:, separation:, isNavigationPresented: Binding<Bool>, asideLabel:) { main } header: { … } navigation: { … } aside: { … } footer: { … } announcement: { … } navigationBar: { … }. A layout of its own, not NavigationSplitView, whose column widths and collapsing cannot be set from the tokens. The window's class comes from the width of the scene (a GeometryReader at the root, in points; Dynamic Type does not change it) and is published as the environment value \.tanreiWindowClass, which a search or a side sheet inside the shell reads. scroll: .page is one ScrollView holding the announcement, the main content, a stacked aside and the footer, with the header in .safeAreaInset(edge: .top) and the navigation and the aside beside it; .main keeps the footer in .safeAreaInset(edge: .bottom). The header's height is published as \.tanreiAppShellScrollOffset and applied as the scroll view's content margin, so ScrollViewReader.scrollTo rests below it. Regions are exposed with accessibilityElement(children: .contain) and one entry each in a custom rotor, the aside labelled by asideLabel. No skip link: VoiceOver's rotor and Full Keyboard Access's group navigation move between regions. TanreiAppShell(navigationOpen, onNavigationOpenChange, modifier, scroll, headerPosition, mainWidth, asideOrder, footerHidden, separation, asideLabel, onWindowClassChange, announcement = {}, header = {}, navigation = {}, aside = {}, footer = {}, navigationBar = {}) { contentPadding -> main }, in the manner of a scaffold with a custom Layout. The window's class comes from LocalWindowInfo.current.containerSize in dp and is provided as LocalTanreiWindowClass. scroll = Page nests the main content, a stacked aside and the footer in one vertical scroll with the header pinned through a nested-scroll connection; the measured header height is provided as LocalTanreiAppShellScrollOffset and as contentPadding. Regions are exposed with Modifier.semantics { isTraversalGroup = true; paneTitle = … }, the aside's from asideLabel. System insets through WindowInsets.safeDrawing. No skip link: TalkBack and keyboard traversal groups move between regions. TanreiAppShell({ required Widget main, Widget? announcement, header, navigation, aside, footer, navigationBar, bool navigationOpen, bool footerHidden = false, ValueChanged<bool>? onNavigationOpenChanged, … }), a widget of its own in the manner of a scaffold (CustomMultiChildLayout), not Scaffold, whose drawer and bar slots draw Material's own. The window's class comes from MediaQuery.sizeOf(context).width in logical pixels and is inherited as TanreiWindowClass.of(context). scroll: page is a CustomScrollView with a pinned header sliver; the header's height is inherited as TanreiAppShellScrollOffset.of(context) and applied as scroll padding for Scrollable.ensureVisible. Regions are Semantics(container: true, explicitChildNodes: true, label: …) in a FocusTraversalGroup with an ordered policy (APS-22). Insets from MediaQuery.viewPaddingOf. No skip link, except on Flutter for the web, which is not a target of this package. <AppShell navigationOpen onNavigationOpenChange header={…} navigation={…} aside={…} footer={…} footerHidden announcement={…} navigationBar={…} menuButton={…}>{main}</AppShell>. The window's class comes from useWindowDimensions().width and is given by context (useWindowClass()). scroll="page" is one ScrollView with stickyHeaderIndices for the header; the header's height is measured with onLayout and given by context (useAppShellScrollOffset()). Regions carry accessibilityRole where the platform has one (header, menu, summary) and accessibilityLabel; the order of APS-22 is the order of the rendered views. Insets from the safe-area context the package already depends on. The layout direction is the app's. No skip link.

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-app-shell-aside-gap sys.space.space300 24px
--tanrei-comp-app-shell-aside-padding-inline sys.space.space200 16px
--tanrei-comp-app-shell-aside-width — 240px
--tanrei-comp-app-shell-breakpoint-expanded sys.breakpoint.expanded 1024px
--tanrei-comp-app-shell-breakpoint-medium sys.breakpoint.medium 600px
--tanrei-comp-app-shell-container-color sys.color.surface #ffffff
--tanrei-comp-app-shell-content-color sys.color.on-surface #1b1b1b
--tanrei-comp-app-shell-footer-padding-block sys.space.space400 32px
--tanrei-comp-app-shell-gutter-compact sys.space.space200 16px
--tanrei-comp-app-shell-gutter-expanded sys.space.space400 32px
--tanrei-comp-app-shell-gutter-medium sys.space.space300 24px
--tanrei-comp-app-shell-main-max-width — 768px
--tanrei-comp-app-shell-main-padding-block sys.space.space400 32px
--tanrei-comp-app-shell-outline-color sys.color.outline-variant #000000 · 8%
--tanrei-comp-app-shell-outline-width sys.border.hairline 1px
--tanrei-comp-app-shell-separation sys.behavior.separation 0
--tanrei-comp-app-shell-skip-link-container-color sys.color.surface-container-high #f3f3f3
--tanrei-comp-app-shell-skip-link-container-elevation sys.elevation.level3 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 1px 0px rgb(0 0 0 / 0.04), 0px 4px 8px -4px rgb(0 0 0 / 0.04), 0px 16px 24px -8px rgb(0 0 0 / 0.06)
--tanrei-comp-app-shell-skip-link-container-padding-block sys.space.space150 12px
--tanrei-comp-app-shell-skip-link-container-padding-inline sys.space.space200 16px
--tanrei-comp-app-shell-skip-link-container-shape sys.shape.corner.small 6px
--tanrei-comp-app-shell-skip-link-offset sys.space.space100 8px
--tanrei-comp-app-shell-skip-link-label-typescale sys.typescale.label 500 14px/1.4286

测试样例

作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。

生成自 specs/app-shell.spec.json, tokens/comp/app-shell.tokens.json, design/tokens/

指南

三种窗口类别

外壳只根据窗口的宽度,把窗口归入三种类别之一。

类别 宽度 导航 侧栏 菜单按钮 导航栏 两侧边距
compact 小于 600 打开时盖在内容之上 主内容的上方或下方 显示 显示 16
medium 600 到 1023 抽屉为 collapsible 时,是它的那一列图标。否则与 compact 相同 上方或下方 显示,除非那一列图标已经显示 不显示 24
expanded 1024 及更宽 位于内容旁边 位于内容旁边的末尾边 不显示 不显示 32
  • 这些宽度就是令牌 sys.breakpoint.medium 和 sys.breakpoint.expanded。设置了 responsive 的导航抽屉读取的是同样两个令牌,所以框架和抽屉一起变化。
  • 在 Web 上,宽度以 rem 为单位比较。读者放大页面时,在各列窄到无法阅读之前,就会得到更窄的那种排列。
  • windowclasschange 告诉你类别,windowClass 保存着它。外壳不负责切换的东西,由你根据它来切换:搜索的呈现方式、侧边面板的变体、大纲的形式。
  • 在 Web 上,框架里的一切还会继承 --tanrei-app-shell-window-class,所以样式表可以用样式容器查询来跟随类别,不需要脚本。

菜单按钮

在页眉里放一个纯图标的 Button,并用 data-app-shell-menu 标记它。只有当导航在布局中没有位置时,外壳才显示它。按下它,抽屉会在内容之上打开,焦点移入抽屉。关闭抽屉时,焦点回到这个按钮。按钮会说明导航是否已展开。

navigationOpen(navigation-open)保存这个状态,navigationopenchange 在用户改变它时告诉你。窗口类别一变,导航就会关闭。

滚动的是什么

scroll 滚动的是什么 用于
page(默认) 窗口滚动整个页面。页眉是粘性的,导航和侧栏留在内容旁边的视野之内。 供人阅读的页面:文档、文章、设置。
main 框架正好是窗口的大小,只有主内容区域滚动。 各个窗格分别滚动的应用:邮件客户端、编辑器。
  • 优先使用 page。页面内查找、指向标题的链接、回到读者先前所在的位置,以及手机浏览器会收起的工具栏,都与没有外壳的页面一样工作。
  • headerPosition: static 让页眉随页面一起滚走。
  • 在 Web 上使用 scroll: main 时,请把主内容元素的 id 写进应用栏的 scroll-target。
  • 在 Web 上使用 scroll: main 时,画在主内容上方或下方的侧栏(在 compact 或 medium 窗口中)在主内容区域滚动时停在原处;它目前还不会随之滚动。应当随页面滚动的内容要放在 <main> 里面。
  • 在 Web 上,对应的 JavaScript 属性是 scrolls,因为每个元素都已经有一个 scroll() 方法。属性(attribute)仍是 scroll。

指向标题的链接停在页眉下方

外壳会测量粘性页眉,并给页面的滚动视图留出同样大小的内边距。指向标题的链接,以及获得键盘焦点的元素,都停在页眉的下方,绝不会被它盖住。在 Web 上,同一段距离也是根元素上的 --tanrei-app-shell-scroll-offset,供你自己的粘性部分使用,例如表格的表头行。在外壳里,请让 --tanrei-prose-scroll-offset 保持为 0。

什么叠在什么之上

  • 页眉和导航栏叠在从它们下方滚过的一切之上,页脚也不例外。主内容区域、侧栏和页脚里的东西,无论 z-index 设为多少,都不会升到它们上面:每个区域都把自己所含内容的层叠关系限制在区域之内。
  • 菜单、弹出层、工具提示、消息条、对话框和面板叠在整个外壳之上。Tanrei 的这些组件会自己画在那里:在 Web 上,就是浏览器的顶层(top layer)。你自己的浮层也请这样画:用 popover,或用 showModal() 打开的 <dialog>,不要靠很大的 z-index。
  • 在 Web 上,外壳本身就是一个独立的层叠上下文,所以外壳内部的任何层级都不会和你的页面在外壳之外画的东西相争。

主内容区域的宽度

  • mainWidth: contained(默认)让内容的宽度不超过 768,并让它在可用空间里居中。用于供人阅读的页面。
  • mainWidth: full 占满两侧边距之内的全部空间。用于表格、看板或编辑器。
  • 无法收窄的内容,例如很宽的表格或一行代码,在它自己的框内滚动。它绝不会把框架撑宽。

侧栏

  • 在 expanded 窗口中,侧栏位于主内容旁边,宽 240,页面滚动时留在视野之内。
  • 在更窄的窗口中,它并入主内容所在的那一列。asideOrder: before 把它画在主内容上方,适合页面的大纲。after(默认)把它画在下方,适合相关链接。
  • asideOrder 同时也是阅读顺序。在 Web 上,请在标记中把 <aside> 写在 <main> 的那一侧。
  • 用 asideLabel 为侧栏命名,使用你的语言。
  • 放在侧栏里的侧边面板保有自己的宽度和边线。请根据 windowclasschange 设置它的变体。

页脚

  • 填写了页脚区域,页脚就会显示。不填写,就没有页脚,也没有地标。
  • footerHidden(footer-hidden)把已填写的页脚隐藏起来,而不必把它从标记中拿掉。用在一项任务而不是网站某一页的界面上:结账、登录、编辑器。这时页脚不占空间,不画线,不是地标,键盘也不会在其中停靠;导航和侧栏一直留在视野内,直到页面末尾。
  • 焦点在页脚中时隐藏页脚,焦点会移到主内容区域。

区域之间的线

separation 绘制什么
auto(默认) 设计系统所选的方式:默认系统中是线。
divider 一条细线,画在页脚上方、位于内容旁边的侧栏的起始边,以及主内容与上下排列的侧栏之间。
gap 不画线。间距不变。

这些线不占空间。外壳不在页眉、导航、公告或导航栏周围画线:这些组件自己绘制边线。页眉中的应用栏把底边作为留在原位的区域的边缘,在静止时就画出,页面在它下方滚动时也保持;如果只想在有内容位于应用栏下方时显示它,把应用栏的 dividerVisibility 设为 scroll。

推荐

  • 每个页面有一个 <main>,并给它跳过链接所指向的那个 id。
  • 用你的语言写跳过链接的文字。Tanrei 不提供任何文字。
  • 在导航区域使用设置了 responsive 的导航抽屉。

避免

  • 不要把页面的标题、描述或操作放进外壳。它们属于页头,位于主内容区域的最前面。
  • 不要把文章的结尾部分(上一篇和下一篇、编辑链接)放进页脚区域。它是主内容的结尾。页脚区域留给属于整个网站的内容。
  • 不要用 CSS 调整区域的顺序。标记的顺序就是焦点的顺序。

无障碍

角色
none
标注
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 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
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 / 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.

生成自 specs/app-shell.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • 每个有内容的区域就是它对应的地标:横幅(banner)、导航(navigation)、主内容(main)、补充内容(complementary)和内容信息(contentinfo)。区域所用的元素本身不提供这个地标时,外壳会给它相应的角色。
  • 跳过链接是键盘的第一个停靠点。沿着它前往,焦点会移到主内容区域本身,区域的顶部位于页眉下方的视野之内。再按一次 Tab,就到达内容里的第一个控件。
  • Tab 按阅读顺序经过各个区域;在每一种窗口类别下,无论布局方向是从左到右还是从右到左,这个顺序都一样。没有显示的区域没有停靠点。
  • 链接的目标和获得焦点的元素,绝不会留在粘性页眉或导航栏的下面。
  • 窗口类别改变时,如果焦点原先所在的东西不再显示(例如菜单按钮),焦点会移到主内容区域。
  • 隐藏的页脚(footerHidden)不在无障碍树中,键盘也不会在其中停靠。
  • 在宽 320 的窗口里把文本放大到 200% 时,框架是单独一列,没有任何内容需要横向滚动。
  • 在强制颜色模式下,各条线和跳过链接的框以系统的文本颜色绘制。
  • 在从右到左的布局中,导航位于右侧,侧栏位于左侧。焦点的顺序不变。

需要你做的事

  • 每个页面正好一个 <main>。一个都没有或者多于一个时,调试构建会报告。
  • 侧栏的名称(asideLabel),以及导航抽屉和导航栏上的 label。页面上有两个导航地标,就需要两个名称。
  • 跳过链接的文字和菜单按钮的标签。
  • 每个页面一个 1 级标题,位于主内容区域:也就是页头的标题。