サイドシート
補足のコンテンツや副次的な作業を、末尾側の端のパネルに見出し、閉じるボタン、アクション(省略可)とともに表示します。幅の広いウィンドウではコンテンツの横に、コンパクトなウィンドウではコンテンツの上に重ねて表示します。
- カテゴリ
- コンテインメント
- トークン
comp.side-sheet- レベル
- 合成
- 構成要素
- Modal layer
- Web
tanrei-side-sheetschedule - SwiftUI
TanreiSideSheetschedule - Jetpack Compose
TanreiSideSheetschedule - Flutter
TanreiSideSheetschedule - React Native
SideSheetschedule
生成元 docs/status/status.json, specs/side-sheet.spec.json
仕様は改訂済み、各プラットフォームは追従待ち
このページは、サイドシートのアクションと閉じるボタンを仕様の現在の形で説明しています。ダイアログと同じ形です。アクションは、役割に応じて置かれる全幅の Tonal ボタンの行です。それぞれに confirming か dismissive の印を付け、モーダルのシートには少なくとも 1 つ必要です。閉じるボタンは小さい Tonal のボタンで、closable を false にしない限り描かれます。アクションの上と同じく、ヘッダーの下にも区切り線が常に引かれ、アクションはオンスクリーンキーボードの上に留まります。Web コンポーネントはすでにこのすべてに対応しており、デモもそれを示しています。上の表で Web が「追従待ち」のままなのは、改訂の確認が済んでいないためです。SwiftUI、Compose、Flutter、React Native も「追従待ち」です。追従するまでは以前の版(閉じるボタンは standard のアイコンボタン、アクションは渡した順に先頭側の端から 1 行に並び、どちらの形でも省略でき、間隔は 8)を提供しています。
行の終わりに置いたスタンダードのシート
検索結果
選んだラベルがすべて付いた、過去 30 日間の結果を表示します。
サイドシートは制御コンポーネントで、自分で閉じることはありません。open を設定して表示します。人が閉じようとすると、シートはその理由を添えて close を発火するので、それを受けて利用者側で open を false に戻します。open がバインディングである SwiftUI では、シートが自分で false を書き込み、そのあとで onClose を呼びます。ドラッグのあとに open を true のままにしておくと、シートは元の位置へ滑って戻ります。
close が伝える理由は次の 5 つです(表記はプラットフォームの流儀に従います)。
| 理由 | 人がしたこと | 対象 |
|---|---|---|
close |
閉じるボタンを押した | スタンダードとモーダル |
scrim |
モーダルのシートの横の暗い部分を押した | モーダル |
escape |
Escape を押した | モーダル |
back |
プラットフォームの「戻る」を使った(Android の戻るボタンやジェスチャー、iOS のアクセシビリティの「戻る」ジェスチャー) | モーダル |
drag |
シートを端の方へ幅の半分を超えてドラッグした、またはその向きにフリングした | モーダル |
スタンダードのシートは Escape、「戻る」、ドラッグを無視します。利用者側のコントロールで閉じるのでない限り、人が閉じる手段は閉じるボタンだけです。
シートには variant で選ぶ 2 つの形があり、1 つのプロダクトでたいてい両方を使います。ウィンドウの幅が足りればスタンダード、足りなければモーダルです。
- 既定のスタンダードはレイアウト領域で、オーバーレイではありません。開いている間は、置いた行の終わりに自分の幅を取り、横のコンテンツの幅が変わります。スクリムを描かず、フォーカスを中にとどめることもありません。
- モーダルはモーダルなオーバーレイです。スクリムとともに末尾側の端からコンテンツの上に滑り込み、フォーカスを中にとどめ、閉じたら返します。
シートは、headline と、closable が true(既定)の間は closeLabel を名前とする閉じるボタンを並べたヘッダーを描きます。コンテンツはヘッダーの下に、actions スロットに入れた Tanrei のボタンは下端に並びます。ボタンにはそれぞれ、作業を完了するもの(適用、保存)なら confirming、作業から抜けるもの(キャンセル)なら dismissive の印を付けます。シートはこの印で置き場所を決めるので、渡す順番は問いません。variant を設定していないアクションは Tonal で描きます。モーダルのシートには少なくとも 1 つのアクションが必要で、スタンダードのシートにはなくても構いません。アクションが自分でシートを閉じることはないので、アクションのハンドラーから閉じます。
開いたときにフォーカスを受け取る要素は、プラットフォームの印で指定します。Web では autofocus、SwiftUI では .defaultFocus か @AccessibilityFocusState、Compose では initialFocus に渡す FocusRequester、Flutter では autofocus: true、React Native では initialFocusRef です。SwiftUI では、.defaultFocus がキーボードのフォーカスを、initialFocus に渡す @AccessibilityFocusState のバインディングが VoiceOver のフォーカスを移します。指定しなければフォーカスはシートそのものへ移り、見出しが最初に読み上げられます。
Web と React の例は現在の仕様に沿っています。SwiftUI、Compose、Flutter、React Native は追従待ちで、以前の版のサイドシートをまだ提供しています。その版には confirming と dismissive の印も、Tonal の既定もないので、これらの例では各アクションに text のバリアントを指定し、主となるアクションを最初に置いています。追従するまで、例はその版のものです。
<tanrei-button id="filters" variant="outlined">絞り込み</tanrei-button>
<!-- スタンダード:flex の行の最後の要素として置きます。見出しを名前とする <aside> です。 -->
<div style="display: flex">
<main style="flex: 1">…</main>
<tanrei-side-sheet headline="絞り込み" close-label="絞り込みを閉じる">
<p>選んだラベルがすべて付いた、過去 30 日間の結果を表示します。</p>
<!-- confirming は行の終わりに置かれます。variant がなければどちらも Tonal です。 -->
<tanrei-button slot="actions">リセット</tanrei-button>
<tanrei-button slot="actions" confirming>適用</tanrei-button>
</tanrei-side-sheet>
</div>
<script type="module">
import '@tanrei/web/components/side-sheet';
const sheet = document.querySelector('tanrei-side-sheet');
document.querySelector('#filters').addEventListener('click', () => (sheet.open = true));
// シートは自分では閉じません。detail.reason は close、scrim、escape、back、drag のどれかです。
sheet.addEventListener('close', (e) => (sheet.open = false));
// 幅が 840px より狭いウィンドウでは、同じシートをスクリムの上のモーダルな <dialog> にします。
matchMedia('(width < 840px)').addEventListener('change', (e) => (sheet.variant = e.matches ? 'modal' : 'standard'));
</script>import { Button, SideSheet } from '@tanrei/react';
<SideSheet
variant={wide ? 'standard' : 'modal'}
open={open}
headline="絞り込み"
closeLabel="絞り込みを閉じる"
onClose={() => setOpen(false)}
>
<p>選んだラベルがすべて付いた、過去 30 日間の結果を表示します。</p>
<Button slot="actions" onClick={reset}>リセット</Button>
<Button slot="actions" confirming onClick={apply}>適用</Button>
</SideSheet>import Tanrei
// スタンダード:HStack の最後に置くビューです。閉じるとバインディングに false が書き込まれ、そのあとで onClose が呼ばれます。
HStack(spacing: 0) {
ResultsView()
TanreiSideSheet("絞り込み", isPresented: $filtersShown, closable: true, closeLabel: "絞り込みを閉じる") {
FilterForm()
} actions: {
TanreiButton("適用", variant: .text) { apply() }
TanreiButton("リセット", variant: .text) { reset() }
}
}
// モーダル:覆うビューに付けるモディファイアーで、tanreiDialog と同じように表示します。
ResultsView()
.tanreiSideSheet("絞り込み", isPresented: $filtersShown, closable: true, closeLabel: "絞り込みを閉じる") { … }import io.github.chnotchy.tanrei.components.TanreiButton
import io.github.chnotchy.tanrei.components.TanreiSideSheet
import io.github.chnotchy.tanrei.tokens.TanreiButtonVariant
import io.github.chnotchy.tanrei.tokens.TanreiSideSheetVariant
// スタンダードは Row の最後の子です。モーダルは専用の Dialog ウィンドウに描きます。
// 理由を受け取る onClose で visible を false にします。
Row {
ResultsScreen(Modifier.weight(1f))
TanreiSideSheet(
visible = filtersShown,
onClose = { filtersShown = false },
headline = "絞り込み",
variant = if (wide) TanreiSideSheetVariant.Standard else TanreiSideSheetVariant.Modal,
closable = true,
closeLabel = "絞り込みを閉じる",
actions = {
TanreiButton(text = "適用", onClick = apply, variant = TanreiButtonVariant.Text)
TanreiButton(text = "リセット", onClick = reset, variant = TanreiButtonVariant.Text)
},
) {
FilterForm()
}
}import 'package:tanrei/tanrei.dart';
// スタンダード:Row の最後に置きます。モーダルのシートは代わりにページを覆う Stack に置きます。
Row(
children: <Widget>[
const Expanded(child: ResultsPage()),
TanreiSideSheet(
open: filtersShown,
onClose: (TanreiSheetCloseReason reason) => setState(() => filtersShown = false),
headline: '絞り込み',
showCloseButton: true,
closeButtonSemanticLabel: '絞り込みを閉じる',
actions: <Widget>[
TanreiButton(variant: TanreiButtonVariant.text, onPressed: apply, child: const Text('適用')),
TanreiButton(variant: TanreiButtonVariant.text, onPressed: reset, child: const Text('リセット')),
],
child: const FilterForm(),
),
],
),import { View } from 'react-native';
import { Button, SideSheet } from '@tanrei/react-native';
// スタンダード:行の最後に置きます。モーダルのシートは透明な Modal で表示します。
<View style={{ flex: 1, flexDirection: 'row' }}>
<Results style={{ flex: 1 }} />
<SideSheet
variant={wide ? 'standard' : 'modal'}
open={open}
onClose={(reason) => setOpen(false)}
headline="絞り込み"
closable
closeLabel="絞り込みを閉じる"
actions={
<>
<Button variant="text" onPress={apply}>適用</Button>
<Button variant="text" onPress={reset}>リセット</Button>
</>
}
>
<FilterForm />
</SideSheet>
</View>モーダルのシート:ボタンで開きます
選んだラベルがすべて付いた、過去 30 日間の結果を表示します。
構成要素
構成要素
standard の上に描いています。| パーツ | 説明 |
|---|---|
1 container | The panel at the end edge, the full height of its parent (standard) or of the window (modal). Standard: part of the layout. Modal: above the content, the corners that face the content rounded. Its color reaches under the system insets at its end, top and bottom. |
2 outline | The start edge of a standard sheet, which sets it apart from the content beside it. |
3 scrim · 任意 | The modal layer's scrim (MDL-1): it dims everything behind a modal sheet; a press on it asks the sheet to close. |
4 header · 任意 | The row at the top that holds the headline and the close button. It stays in place while the content scrolls. |
5 headline · 任意 | What the sheet is about ("Filters"). It is the sheet's accessible name. |
6 close-button · 任意 | An icon-only Button of the small size showing the close glyph at the end of the header, in the variant comp.side-sheet.close-button.variant names (tonal in the default system), as every host's close button is. The sheet places it and chooses its variant; everything else it reads from Button's own tokens. |
7 content | Whatever the product puts in the sheet. The sheet insets it, hands it a content color and scrolls it between the header and the actions; it does not lay it out. |
8 divider · 任意 | Hairlines across the sheet: above the actions, drawn while there are actions; and below the header, drawn whenever the header is in the default system, or only while the content is scrolled away from its top under dividerVisibility scroll. |
9 actions · 任意 | Tanrei Buttons at the bottom of the sheet, each marked confirming or dismissive, laid out as a full-width Button group: a row the actions share equally, or a column when they do not fit. A Button whose variant the product did not set is tonal in the default system. At least one in a modal sheet; optional in a standard one. |
寸法
寸法
| 長さ | トークン | 値(px) |
|---|---|---|
container width | tanrei.comp.side-sheet.container.width | 320 |
container padding-inline | tanrei.comp.side-sheet.container.padding-inline | 24 |
standard の上に描いています。プロパティ
| プロパティ | 型 | 既定値 | 説明 |
|---|---|---|---|
variant | standard | modal | "standard" | Standard sits beside the content and takes its width in the layout (a layout region); modal slides over the content with a scrim and holds focus until it closes (a modal overlay). One product uses both: standard where the window is wide enough, modal where it is not. A design system may add values. |
open | boolean | false | Whether the sheet is shown: for a standard sheet, whether it takes its place in the layout; for a modal one, whether it is presented. The product opens it, and closes it in answer to close; on SwiftUI, where it is a binding, the sheet writes false itself. |
headline | string | "" | What the sheet is about, drawn in its header and used as its accessible name. Required. |
closable | boolean | true | Draws the close button at the end of the header. True by default, in both variants: a standard sheet has no other way for the person to close it, and a modal one shows a way out where the person reads. Set it to false only where the product's own control, or one of the sheet's actions, closes the sheet. |
closeLabel | string | "" | The close button's accessible name ("Close filters"). Required while closable is true, which is the default. |
dividerVisibility | auto | always | scroll | "auto" | When the divider below the header is drawn. always: whenever the header is drawn, at rest as well as while the content is scrolled, as the divider above the actions always is. scroll: only while the content is scrolled away from its top. auto takes comp.side-sheet.divider.visibility, the design system's answer (always in the default system). |
スロット
| スロット | 説明 |
|---|---|
default | The content, below the header. A fixture's content string is plain text, which takes sys.typescale.body in the content color (SSH-10). |
actions | The action items: Tanrei Buttons, each marked confirming or dismissive; at least one in a modal sheet (SSH-16). The sheet places them (SSH-11) and gives those without a variant its own (SSH-17). A fixture fills it through its items. |
イベント
| イベント | 説明 |
|---|---|
close | The sheet asks to be closed: its close button, or for a modal sheet a press on the scrim, Escape, the platform's back or a drag toward its edge. Carries which of these it was, named close, scrim, escape, back or drag (in the platform's casing). |
アイテム
このコンポーネントが繰り返すアイテムの種類です。フィクスチャはアイテムを順に並べ、各プラットフォームはそれぞれの流儀で種類ごとに名前を付けます。
action
One action at the foot of the sheet: a Tanrei Button, placed by the sheet as a full-width Button group places its items and given the sheet's variant unless the product set one.
| プロパティ | 型 | 既定値 | 説明 |
|---|---|---|---|
variant | string | "" | The Button's variant (filled, tonal, elevated, outlined, text, or one a system added). Empty, the default: the sheet gives it the variant comp.side-sheet.actions.variant names, tonal in the default system (SSH-17). Set it to give one action another look. |
disabled | boolean | false | As the Button's disabled. A disabled action keeps its place and its share of the width. |
confirming | boolean | false | The action completes the task (Apply, Save, Show results). Placed at the end of a row and at the top of a column (BGR-13, BGR-14). It decides the place only, never the look (BGR-19). |
dismissive | boolean | false | The action backs out of the task (Cancel, Discard changes). Placed at the start of a row and at the bottom of a column (BGR-13, BGR-14). It decides the place only, never the look (BGR-19). |
| スロット | 説明 |
|---|---|
label | The Button's label, as its default slot. A fixture item's content is this label. |
| イベント | 説明 |
|---|---|
click | The action was activated, as the Button's click. The sheet does not close by itself (SSH-18). |
| 仕様 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| アイテム | tanrei-button | TanreiButton | TanreiButton | TanreiButton | Button |
variant · プロパティ | variant | variant | variant | variant | variant |
disabled · プロパティ | disabled | disabled | enabled | onPressed | disabled |
confirming · プロパティ | confirming | tanreiGroupPlacement(.confirming) | Modifier.confirming() | TanreiButtonGroupItem.placement | confirming |
dismissive · プロパティ | dismissive | tanreiGroupPlacement(.dismissive) | Modifier.dismissive() | TanreiButtonGroupItem.placement | dismissive |
click · イベント | click | action | onClick | onPressed | onPress |
| 備考 | A tanrei-button child with slot="actions": its label is its content. confirming and dismissive are boolean attributes that the sheet reads, as the button group does. A Button without a variant attribute takes the sheet's variant through the internal controller the group already gives its items (SSH-17); the attribute, once written, wins. | A TanreiButton in the actions: builder. The flags are the button group's .tanreiGroupPlacement(_:) modifier (one enum: both cannot be set). A Button created with variant: nil reads the sheet's variant from an environment value the sheet sets (internal to the package). | A TanreiButton in the actions lambda, whose receiver is the button group's scope, so the flags are its Modifier.confirming() and Modifier.dismissive(). enabled is disabled's inverse. variant: TanreiButtonVariant? = null takes the sheet's from a CompositionLocal (internal to the library). | A TanreiButton in actions:; a null onPressed disables it. The flags are the button group's TanreiButtonGroupItem(placement:, child:) (one enum: both cannot be set). A Button with variant: null takes the sheet's from an InheritedWidget (internal to the package). | A Button in the actions prop. confirming and dismissive are props the sheet reads from its actions, as the button group reads them. A Button with no variant takes the sheet's from a React context (internal to the package). |
状態
振る舞い
EARS 記法による要件です。各プラットフォームの実装は、この ID に対応するテストで検証されます。要件の原文は仕様ファイルのものをそのまま掲載しています。
| ID | 要件 |
|---|---|
SSH-1 | WHILE variant is standard THE side sheet SHALL take its width in the layout at the end of the row the product places it in WHILE open is true, and no width and no presence to assistive technology or focus WHILE open is false; WHEN open changes it SHALL slide in or out at the end edge while the content beside it resizes, over comp.side-sheet.motion.enter-duration with comp.side-sheet.motion.enter-easing when opening and comp.side-sheet.motion.exit-duration with comp.side-sheet.motion.exit-easing when closing, and at once where motion is reduced. A standard sheet SHALL draw no scrim, SHALL NOT keep focus inside it, SHALL let Tab and Shift+Tab move focus out of it without closing it, and SHALL ignore Escape, the platform's back and drags (a standard sheet is a layout region, not an overlay). |
SSH-2 | WHEN open becomes true WHILE variant is modal THE side sheet SHALL slide in from the window's end edge over the content on its modal layer, fading the layer's scrim in comp.side-sheet.scrim.color and comp.side-sheet.scrim.blur in over the rest of the window in step with it (MDL-1, MDL-7), over comp.side-sheet.motion.enter-duration with comp.side-sheet.motion.enter-easing; WHEN open becomes false it SHALL slide out to the end edge from wherever it is and fade the scrim out, over comp.side-sheet.motion.exit-duration with comp.side-sheet.motion.exit-easing. WHERE motion is reduced it SHALL fade in and out without sliding, or appear and disappear at once on a platform whose reduced-motion setting removes every animation (Android's). WHILE it is open THE side sheet SHALL make everything behind it inert and keep focus inside it, as its modal layer does (MDL-2, MDL-4). |
SSH-3 | WHEN open changes from false to true THE side sheet SHALL move focus to the element in it that the product marked for initial focus (the platform's own mark, named in the platform notes) or, without one, to its container, so that assistive technology reads the headline first; WHEN it closes while focus is inside it THE side sheet SHALL return focus to the element that had it when the sheet opened or, where that element no longer exists or cannot take focus, to the platform's default. Focus SHALL move when the sheet opens or closes, not when its motion ends. This holds for both variants (a modal sheet moves and returns focus as its modal layer does, MDL-3, MDL-5); a standard sheet mounted with open true SHALL NOT move focus, and a modal sheet mounted with open true SHALL move focus in as if it had just opened, because it makes everything behind it unfocusable and focus must be somewhere it can reach. |
SSH-4 | WHILE variant is modal and the sheet is open, WHEN the user presses the scrim, presses Escape, uses the platform's back, as its modal layer answers them (MDL-6), or drags the sheet toward its end edge past half its width or flings it that way, THE side sheet SHALL emit close exactly once with the reason scrim, escape, back or drag; a drag SHALL follow the finger or pointer once it passes the platform's touch slop, and a released drag that does not ask to close SHALL slide the sheet back over comp.side-sheet.motion.enter-duration with comp.side-sheet.motion.enter-easing (at once where motion is reduced). The fling, the touch slop and the stillness after which a release is no fling are those BSH-9 names for the bottom sheet: the platform's own, or 0.5 points per millisecond, 8 points and 40 milliseconds where the platform names none. |
SSH-5 | WHILE closable is true THE side sheet SHALL draw at the end of its header an icon-only Button of the small size showing the close glyph and named by closeLabel, in the variant comp.side-sheet.close-button.variant names, given through the channel a Button reads from the component that places it, and otherwise unrestyled: it reads Button's own tokens, answers hover, focus and press as any Button does and keeps its own minimum target (BTN-5); WHEN the user activates it THE side sheet SHALL emit close exactly once with the reason close, in either variant. WHILE closable is true and closeLabel is empty it SHALL still draw the button, and in debug builds report that the button has no accessible name. |
SSH-6 | WHEN the side sheet has emitted close THE side sheet SHALL be closed through its binding where open is one (SwiftUI), and by the product setting open to false where open is a plain value (web, Compose, Flutter, React Native); a released drag that asked to close SHALL hold where it left the sheet for one frame, then slide out from there IF open has become false, or slide back IF open is still true (the product declined to close it). |
SSH-7 | THE side sheet SHALL be comp.side-sheet.container.width wide beside the end system inset — WHILE variant is modal never wider than the window less sys.target.touch, so that a strip of scrim stays pressable — and the full height of its parent (standard) or of the window (modal), filled with its variant's container color (comp.side-sheet.container.color.standard or comp.side-sheet.container.color.modal), raised by its variant's elevation (comp.side-sheet.container.elevation.standard, comp.side-sheet.container.elevation.modal), with the corners at its start edge rounded by its variant's shape (comp.side-sheet.container.shape.standard, comp.side-sheet.container.shape.modal) eased by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0) and square corners at its end edge, and edged at its start by comp.side-sheet.outline.width of its variant's outline color (comp.side-sheet.outline.color.standard, comp.side-sheet.outline.color.modal); right-to-left layouts SHALL put it at the left edge, mirrored. |
SSH-8 | THE side sheet SHALL lay out, top to bottom: WHILE headline is not empty or closable is true, the header, a row at least comp.side-sheet.header.height high holding the headline from comp.side-sheet.container.padding-inline after the start edge and, WHILE closable is true, the close button ending comp.side-sheet.header.padding-inline-end before the end edge, comp.side-sheet.header.gap after the headline, both centred in the row; then the content, inset by comp.side-sheet.container.padding-inline from the start and end edges, starting directly below the header or, without one, comp.side-sheet.container.padding-block below the top edge; then, WHILE the sheet has at least one action, the divider and the actions pinned to the bottom (SSH-11), or else comp.side-sheet.container.padding-block of space below the content. It SHALL extend its container under the system insets at its end, top and bottom and place the header, content and actions inside them. |
SSH-9 | WHILE headline is not empty THE side sheet SHALL draw it in comp.side-sheet.headline.typescale and comp.side-sheet.headline.color, wrapping onto more lines rather than truncating and growing the header with it, and SHALL expose it as a heading. THE side sheet SHALL set the headline's letter-spacing by the script of its text, as every title is set (PRS-20): no negative letter-spacing WHERE its language is Chinese, Japanese or Korean, and none WHERE its script joins its letters or stacks marks on them. |
SSH-10 | THE side sheet SHALL hand its content comp.side-sheet.content.color as the platform's inherited foreground (the web's inherited color, SwiftUI's foreground style, Compose's content color, Flutter's default text and icon style, React Native's Tanrei text context); plain text given as content, where the platform must style it, SHALL take sys.typescale.body. WHILE the content is taller than the room between the header and the actions THE side sheet SHALL scroll it there, keeping the header and the actions in place, and SHALL let assistive technology and keyboard focus scroll it. |
SSH-11 | WHILE the sheet has at least one action THE side sheet SHALL draw a divider of comp.side-sheet.divider.width in comp.side-sheet.divider.color across its full width below the content, whether or not the content scrolls, and below it lay out its actions as a standard Button group lays out its items (BGR-12 to BGR-19), comp.side-sheet.actions.gap apart, comp.side-sheet.container.padding-inline from each side edge and comp.side-sheet.actions.padding-block above and below them, in the order the group gives them — dismissive at the start and confirming at the end of a row, confirming on top and dismissive at the bottom of a column, unflagged actions between them in the order given — with focus and reading order following that order (BGR-15). WHILE comp.side-sheet.actions.arrangement is fill it SHALL make them a full-width group across the sheet's inner width, the actions sharing a row equally (BGR-17) or each as wide as the column; WHILE it is start it SHALL keep them at their own widths, the row aligned to the start edge, and a column as wide as its widest action and aligned to the start. It SHALL place them in a row WHILE every action's label fits its share on one line (fill) or the row at its own widths fits the inner width (start), and otherwise in a column, re-deciding when the sheet's width or the text size changes. WHEN the layout direction is right-to-left it SHALL place them as a Button group places them right to left (BGR-8). It SHALL hide the divider from assistive technology. |
SSH-12 | THE side sheet SHALL expose its container, WHILE variant is modal, as its modal layer's modal dialog (MDL-10) named by headline and, WHILE variant is standard, as a complementary region named by headline (the web's aside landmark); WHILE headline is empty it SHALL still render, and in debug builds report that it has no accessible name. |
SSH-13 | WHEN variant names a value that the active design system does not have THE side sheet SHALL render with the default value's tokens, and in debug builds report the unknown value. |
SSH-14 | WHILE the text size is enlarged THE side sheet SHALL keep its width, grow its header with the headline (its height is a minimum), scroll its content (SSH-10), stack its actions once a label no longer fits its share (SSH-11), and SHALL NOT scale the close button beyond what the Button itself does. |
SSH-15 | WHEN a dialog, a menu or another modal sheet opens from a modal side sheet THE side sheet SHALL stay below it, become inert with the rest of the window, and on the other overlay's closing receive back the focus that overlay returns, as its modal layer does (MDL-8, MDL-9); Escape SHALL dismiss only the topmost overlay. |
SSH-16 | WHILE variant is modal, IF the sheet opens with no action THE side sheet SHALL still open and lay out its other parts, and in debug builds SHALL report as an error that a modal side sheet must offer at least one action; a platform whose modal sheet has an API of its own that can require an action (a required, non-empty builder or list of actions) SHALL require it there, and one that can count its actions only when the sheet opens SHALL count them then. WHILE variant is standard THE side sheet SHALL accept having no action and report nothing, and SHALL then draw neither the divider above the actions nor the actions (SSH-8). |
SSH-17 | WHILE an action's variant is empty THE side sheet SHALL give that Button the variant comp.side-sheet.actions.variant names, through the channel a Button reads from the component that places it, so that the Button renders from that variant's tokens and states as it would with the variant set; WHILE an action's variant is set THE side sheet SHALL leave it as set. THE side sheet SHALL NOT change an action's size or tokens, and SHALL NOT change its look or emphasis for its confirming or dismissive (BGR-19). |
SSH-18 | WHEN the user activates an action THE side sheet SHALL let the Button emit its own click and SHALL NOT close, emit close or otherwise answer it itself, whether the action is confirming, dismissive or neither; the product closes the sheet from the action's handler, as it does in answer to close. |
SSH-19 | WHILE the header is drawn THE side sheet SHALL draw a divider of comp.side-sheet.divider.width in comp.side-sheet.divider.color across its full width below the header, hidden from assistive technology: WHILE its divider visibility (SSH-23) is always, at rest as well as while the content is scrolled, as the divider above the actions is drawn whether or not the content scrolls (SSH-11); WHILE it is scroll, only WHILE the content is scrolled away from its top. WHILE there is no header it SHALL draw none there. It SHALL show and hide this divider at once, without motion. |
SSH-20 | WHILE the on-screen keyboard covers the bottom of the sheet THE side sheet SHALL keep its header in place, raise its divider and actions to sit directly above the keyboard, scroll the content in the room left between them, and scroll the focused element into that room; WHEN the keyboard hides it SHALL return the actions to the bottom edge. It SHALL move them as the platform moves content for its keyboard, and at once where motion is reduced. |
SSH-21 | THE side sheet 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 side sheet is shown THE side sheet SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state and its focus. |
SSH-22 | THE side sheet SHALL lay out its headline as text of its own direction, isolated from the text around it: each takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a number with its sign or unit, a telephone number and an address keep their own order in a right-to-left layout and reorder nothing beside them; THE side sheet SHALL still place the headline and align its lines by the layout direction, not by the text's own. |
SSH-23 | THE side sheet SHALL draw the divider below its header (SSH-19) as its dividerVisibility names: WHILE dividerVisibility is auto, as comp.side-sheet.divider.visibility names, which follows sys.behavior.divider-visibility unless a design system sets the side sheet apart; WHILE dividerVisibility is always or scroll, as that value names, whatever the tokens say. WHEN dividerVisibility, or the token it follows, changes THE side sheet SHALL draw the other form at once, moving no part and keeping the scroll position and focus; WHEN dividerVisibility names a value the side sheet does not define THE side sheet SHALL treat it as auto and in debug builds report the unknown value. A platform that cannot draw a choice SHALL fall back to the first value, always, and state that in its README. |
SSH-24 | WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE side sheet SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on its container (SSH-12) — its dialog WHILE variant is modal, its complementary region otherwise — an id reference by the elements it names in the host's tree or a tree around it, a description after its own. WHILE headline is not empty it is the container's name, over a name given so; WHILE headline is empty, a name given so is the container's name and SHALL NOT be reported as having none in debug builds. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE side sheet SHALL take it off that element. |
プラットフォーム別の名前対応
パリティは API が同一であることを意味しません。同じ契約を、各プラットフォームの流儀で表現します。
| 仕様 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| コンポーネント | tanrei-side-sheet | TanreiSideSheet | TanreiSideSheet | TanreiSideSheet | SideSheet |
variant · プロパティ | variant | variant | variant | variant | variant |
open · プロパティ | open | isPresented | visible | open | open |
headline · プロパティ | headline | headline | headline | headline | headline |
closable · プロパティ | closable | closable | closable | showCloseButton | closable |
closeLabel · プロパティ | closeLabel | closeLabel | closeLabel | closeButtonSemanticLabel | closeLabel |
dividerVisibility · プロパティ | dividerVisibility | dividerVisibility | dividerVisibility | dividerVisibility | dividerVisibility |
close · イベント | close | onClose | onClose | onClose | onClose |
| 備考 | closable is a boolean property that defaults to true; its attribute is the inverse not-closable (an HTML boolean attribute cannot default to true), as the chip's removable is. closeLabel is the close-label attribute. close is a CustomEvent whose detail.reason is close, scrim, escape, back or drag; the product sets open to false from it. Slots: default and actions. The actions are laid out by the button group's placement (BGR-13 to BGR-17) inside the shadow root: the sheet's shadow root uses manual slot assignment, assigns its content children to the default slot, and assigns each action to one of three action slots in the order of the orientation, so focus and the accessibility tree follow the placed order (BGR-15); CSS order is not used. Modal: a <dialog> in the shadow root opened with showModal(), its ::backdrop the scrim. Standard: an <aside> named by the headline, placed by the product as the last item of a flex row; its width animates. The close button is a <tanrei-button> in the shadow root, icon-only (the close glyph in its icon slot, its label from close-label), whose variant comes from the resolved comp.side-sheet.close-button.variant through the internal controller the group gives its items. The autofocus attribute on a slotted element marks initial focus. Debug builds (import.meta.env.DEV or the package's dev export) report a modal sheet opened with no assigned action (SSH-16). The keyboard's room (SSH-20) is the visualViewport's height. Insets are env(safe-area-inset-*). Snapshot harness: a 720 × 480 viewport with a surface page; the standard sheet at the end of a row whose other child fills the rest of the width; the modal sheet opened by open, the screenshot being the viewport. dividerVisibility is the divider-visibility attribute (auto, always, scroll); auto reads --tanrei-comp-side-sheet-divider-visibility from the cascade with a style query, and where the browser has none the sheet writes the name it read on its header when it renders. | Lagging: ships the earlier revision — a standard icon button to close, the actions a row from the start edge in the order given, optional in both variants, 8 apart. The target: standard TanreiSideSheet(_ headline:, isPresented: Binding<Bool>, closable:, closeLabel:, initialFocus:, onClose:) { content } actions: { … }, a view the product places at the end of an HStack, with actions: optional. Modal: the modifier .tanreiSideSheet(_ headline:, isPresented:, …, content:, actions:) on the view it covers, with actions: a required builder whose subviews the sheet counts through a variadic view, reporting an empty one in debug builds (SSH-16); presented as tanreiDialog is (on iOS .fullScreenCover with a clear presentation background and the system transition off; Tanrei draws the scrim, the sheet and the drag), and on macOS, which has no full-screen cover, as an overlay of the modified view with the modal trait. Both lay out the actions with the button group's Layout. Closing writes false to isPresented and then calls onClose with the reason. The close button is an icon-only TanreiButton whose variant the sheet sets from its token. Initial focus is .defaultFocus, or an @AccessibilityFocusState binding the product passes as initialFocus (without it, VoiceOver starts at the headline); Escape is .onKeyPress(.escape) and the escape gesture .accessibilityAction(.escape). The keyboard's safe area lifts the actions (SSH-20). Snapshot harness: renders TanreiSideSheetSurface (the sheet and, for a modal sheet, the scrim) in place in a 720 × 480 frame over a surface page. Not yet: the headline's letter-spacing by the script of its text (SSH-9, PRS-20). Not built on this platform yet: dividerVisibility (auto, always, scroll; SSH-23) — the divider below the header is still drawn only while the content is scrolled from its top, as scroll does. | Lagging: ships the earlier revision — a standard icon button to close, the actions a row from the start edge in the order given, optional in both variants, 8 apart. The target: TanreiSideSheet(visible, onClose: (TanreiSheetCloseReason) -> Unit, headline, variant, closable, closeLabel, initialFocus: FocusRequester?, actions: (@Composable () -> Unit)?, content); the caller sets visible to false from onClose. The actions lambda's receiver is the button group's scope, not RowScope, so the flags are its Modifier.confirming() and Modifier.dismissive() and no inert weight or align is offered. Its Layout sees the actions' measurables and, for a modal sheet, reports none in debug builds (SSH-16); variant is a parameter, so the type cannot require actions for the modal form alone. Standard: a composable the product places at the end of a Row; its width animates. Modal: drawn in a Dialog window of its own, as TanreiDialog and the modal drawer are, with the window's own dim off (Tanrei draws the scrim); the window traps focus, answers back and hides the content behind from TalkBack, and the sheet sets a pane title from the headline. The close button is an icon-only TanreiButton whose variant the sheet sets from its token. The keyboard's room is WindowInsets.ime (SSH-20). Snapshot harness: renders TanreiSideSheetContent (the sheet and, for a modal sheet, the scrim) in place in a 720 × 480 dp box over a surface page. Not yet: the headline's letter-spacing by the script of its text (SSH-9, PRS-20). Not built on this platform yet: dividerVisibility (auto, always, scroll; SSH-23) — the divider below the header is still drawn only while the content is scrolled from its top, as scroll does. | Lagging: ships the earlier revision — a standard icon button to close, the actions a row from the start edge in the order given, optional in both variants, 8 apart. The target: TanreiSideSheet(variant:, open:, onClose: ValueChanged<TanreiSheetCloseReason>, headline:, showCloseButton:, closeButtonSemanticLabel:, actions: [...], child:), controlled: the caller sets open to false from onClose. A modal sheet asserts actions non-empty when it opens (SSH-16); the actions are laid out with the button group's render object. Standard: placed at the end of a Row. Modal: a controlled overlay in a Stack over the page, as the modal navigation drawer, with its own scrim and slide, a FocusScope that keeps focus in, the page's semantics blocked, back through PopScope and Escape through Shortcuts/Actions. The close button is an icon-only TanreiButton whose variant the sheet sets from its token; the headline is wrapped in Semantics(header: true). The keyboard's room is MediaQuery.viewInsetsOf(context).bottom (SSH-20). Snapshot harness: a golden of the whole 720 × 480 surface over a surface page. Not yet: the headline's letter-spacing by the script of its text (SSH-9, PRS-20). Not built on this platform yet: dividerVisibility (auto, always, scroll; SSH-23) — the divider below the header is still drawn only while the content is scrolled from its top, as scroll does. | Lagging: ships the earlier revision — a standard icon button to close, the actions a row from the start edge in the order given, optional in both variants, 8 apart. The target: controlled: open, onClose(reason); the caller sets open to false from it. actions is a React node (a Button or a fragment of them); development builds report it empty when a modal sheet opens (SSH-16); the types cannot tie it to variant. The content is children. Standard: a View the product places at the end of a row (flexDirection: 'row'). Modal: presented in a transparent Modal with animationType="none", statusBarTranslucent and navigationBarTranslucent; onRequestClose (Android back, and Escape on a hardware keyboard) is close with the reason back or escape. Drags with PanResponder. The close button is an icon-only Button whose variant the sheet sets from its token; the headline has the header role. The keyboard's height comes from Keyboard events (SSH-20). Insets come from the sheet's own insets prop, falling back to TanreiProvider's insets; the package takes no safe-area dependency. Snapshot harness: renders SideSheetSurface in place in a 720 × 480 view over a surface page. The initial-focus mark is an initialFocusRef prop, as on the bottom sheet and the dialog (SSH-3). Not yet: the headline's letter-spacing by the script of its text (SSH-9, PRS-20). Not built on this platform yet: dividerVisibility (auto, always, scroll; SSH-23) — the divider below the header is still drawn only while the content is scrolled from its top, as scroll does. |
トークン
| トークン | 参照先 | 解決値(ライト) |
|---|---|---|
--tanrei-comp-side-sheet-actions-arrangement | — | 0 |
--tanrei-comp-side-sheet-actions-gap | sys.space.space200 | 16px |
--tanrei-comp-side-sheet-actions-padding-block | sys.space.space200 | 16px |
--tanrei-comp-side-sheet-actions-variant | — | 1 |
--tanrei-comp-side-sheet-close-button-variant | — | 1 |
--tanrei-comp-side-sheet-container-color-modal | sys.color.surface-container-low | |
--tanrei-comp-side-sheet-container-color-standard | sys.color.surface | |
--tanrei-comp-side-sheet-container-elevation-modal | sys.elevation.level2 | 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 2px 0px rgb(0 0 0 / 0.04) |
--tanrei-comp-side-sheet-container-elevation-standard | sys.elevation.level0 | |
--tanrei-comp-side-sheet-container-padding-block | sys.space.space300 | 24px |
--tanrei-comp-side-sheet-container-padding-inline | sys.space.space300 | 24px |
--tanrei-comp-side-sheet-container-shape-modal | sys.shape.corner.large | 12px |
--tanrei-comp-side-sheet-container-shape-standard | sys.shape.corner.none | 0px |
--tanrei-comp-side-sheet-container-width | — | 320px |
--tanrei-comp-side-sheet-content-color | sys.color.on-surface | |
--tanrei-comp-side-sheet-divider-color | sys.color.outline-variant | |
--tanrei-comp-side-sheet-divider-visibility | sys.behavior.divider-visibility | 0 |
--tanrei-comp-side-sheet-divider-width | sys.border.hairline | 1px |
--tanrei-comp-side-sheet-header-gap | sys.space.space100 | 8px |
--tanrei-comp-side-sheet-header-height | — | 64px |
--tanrei-comp-side-sheet-header-padding-inline-end | sys.space.space150 | 12px |
--tanrei-comp-side-sheet-headline-color | sys.color.on-surface | |
--tanrei-comp-side-sheet-motion-enter-duration | sys.motion.role.appear.sheet.enter | 400ms |
--tanrei-comp-side-sheet-motion-enter-easing | sys.motion.easing.enter | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-side-sheet-motion-exit-duration | sys.motion.role.appear.sheet.exit | 350ms |
--tanrei-comp-side-sheet-motion-exit-easing | sys.motion.easing.exit | cubic-bezier(0.3, 0, 1, 1) |
--tanrei-comp-side-sheet-outline-color-modal | — | |
--tanrei-comp-side-sheet-outline-color-standard | sys.color.outline-variant | |
--tanrei-comp-side-sheet-outline-width | sys.border.hairline | 1px |
--tanrei-comp-side-sheet-scrim-blur | comp.modal-layer.scrim.blur | 0px |
--tanrei-comp-side-sheet-scrim-color | comp.modal-layer.scrim.color | |
--tanrei-comp-side-sheet-headline-typescale | sys.typescale.heading-5 | 500 16px/1.5 |
フィクスチャ
基準となる描画パターンです。全プラットフォームが同じ ID でスナップショットを撮り、結果を比較できるようにしています。
variant: standard (2)
standard
open: true headline: Filters closable: true closeLabel: Close filters 720 × 480 のウィンドウに単独で描画standard-actions
open: true headline: Filters closable: false items: 2 720 × 480 のウィンドウに単独で描画variant: modal (4)
modal
variant: modal open: true headline: Filters closable: true closeLabel: Close filters items: 2 720 × 480 のウィンドウに単独で描画modal-single-action
variant: modal open: true headline: Filters closable: true closeLabel: Close filters items: 1 720 × 480 のウィンドウに単独で描画modal-stacked
variant: modal open: true headline: Edit profile closable: true closeLabel: Close profile editor items: 2 720 × 480 のウィンドウに単独で描画action-variants
variant: modal open: true headline: Filters closable: true closeLabel: Close filters items: 2 720 × 480 のウィンドウに単独で描画生成元 specs/side-sheet.spec.json, tokens/comp/side-sheet.tokens.json, design/tokens/
人が取り組んでいるものを置き換えずに、それを支えるコンテンツに使います。リストの絞り込み、選んだ行の詳細、選んだオブジェクトのプロパティなどです。幅の広いウィンドウではコンテンツの横に置くので、絞り込みを変えながら結果が変わる様子を見られます。コンパクトなウィンドウでは同じシートがコンテンツの上に滑り込み、人はそれを終えるか閉じるかしてから元の画面に戻ります。
スマートフォンでは、親指の届く下端からせり上がるボトムシートのほうが合うこともよくあります。答えがないと作業を先に進められない判断にはダイアログを、プロダクトの行き先には先頭側の端に置くナビゲーションドロワーを使います。
| バリアント | どういうものか | 用途 |
|---|---|---|
| スタンダード(既定) | レイアウト領域です。行の終わりに置くパネルで、先頭側の端の細い線でコンテンツと区切ります。開閉に合わせて横のコンテンツの幅が変わります。スクリムを描かず、人がコンテンツで作業している間も開いたままです。 | シートをコンテンツの横に開いたままにできる、幅の広いウィンドウ。 |
| モーダル | モーダルなオーバーレイです。コンテンツ側の角が丸い浮き上がったパネルで、スクリムの上に末尾側の端から滑り込みます。背後はすべて操作不能になり、閉じるまでフォーカスは中にとどまります。 | コンパクトなウィンドウ。先に進む前に終えるか閉じるかしてほしい作業。 |
スタンダードのシートは、スタンダードのナビゲーションドロワーやアプリバーと同じくレイアウトの一部です。Tab で先へ進むたびにパネルが閉じてしまってはキーボードで使えないので、フォーカスが出ても閉じません。モーダルの形はオーバーレイで、モーダルなオーバーレイに求められることがすべて当てはまります。
モーダルのシートは、狭いウィンドウでもウィンドウいっぱいにはなりません。少なくともタッチターゲット 1 つ分は狭くして、押せるスクリムを必ず帯状に残します。デザインシステムはバリアントを追加できます。
| 要素 | 設定 | 補足 |
|---|---|---|
| 見出し | headline |
必須です。「絞り込み」のように、シートが何についてのものかを書きます。シートのアクセシブルネームであり見出しでもあります。切らずに折り返します。 |
| 閉じるボタン | closable、closeLabel |
ヘッダーの終わりに置く、close グリフの small のアイコンだけのボタンで、Tanrei のほかの閉じるボタンと同じく、既定のシステムでは Tonal です(m3-like ではグリフだけ)。既定で描かれます。利用者側のコントロールやアクションで閉じるときは、closable を false(not-closable 属性)にします。シートが描いて close につなぐので、利用者側ではラベルを付けるだけです。 |
| コンテンツ | 既定のスロット | シートで見せるもの。シートは余白を付け、コンテンツの色を渡し、ヘッダーとアクションの間でスクロールさせます。 |
| アクション | actions スロット | それぞれ confirming か dismissive の印を付けた Tanrei のボタンで、区切り線の下に並べます。モーダルのシートでは必須、スタンダードのシートでは省略できます。シートが置き場所を決め、variant のないものは Tonal で描きますが、それ以外に見た目を変えることはありません。 |
アクションはシートの内側の幅を等分し、間は 16 です。1 行に並ぶときは、dismissive が先頭側、confirming が末尾側に置かれ、印のないアクションは渡した順にその間に並びます。ラベルが自分の幅で折り返しそうになると縦に積み、confirming がいちばん上に来ます。フォーカスとスクリーンリーダーは、置かれた順番に従います。1 つのアクションだけ見た目を変えたいときは、そのボタンの variant を設定します。すべてのシートの見た目を変えたいプロダクトは、レイヤで一度だけ設定します(actions.variant、先頭側に寄せたコンパクトな行なら actions.arrangement、閉じるボタンなら close-button.variant)。
コンテンツがスクロールしても、ヘッダーとアクションは動きません。アクションの上の区切り線はいつも描き、既定のシステムではヘッダーの下にも同じ区切り線をいつも描きます。どちらも、ページの横に留まるパネルの縁だからです。dividerVisibility を scroll(Web では divider-visibility)にすると、ヘッダーの下の線はコンテンツが先頭からスクロールしている間だけ引かれます。cupertino と m3-like は、すべてのシートでこちらを選んでいます。オンスクリーンキーボードがシートの下を覆っている間は、アクションはそのすぐ上まで上がり、コンテンツは残った場所でスクロールします。シートの色は末尾側、上、下のシステムインセットの下まで届き、ヘッダー、コンテンツ、アクションはその内側に収まります。右から左のレイアウトでは、左右を反転して左端に置きます。
- スタンダードを開く。 シートが末尾側の端から滑り込み、横のコンテンツが場所を空けます。フォーカスは指定した要素へ、なければシートへ移り、見出しが最初に読み上げられます。最初から開いた状態で表示したシートは、フォーカスを動かしません。
- スタンダードの横で作業する。 Tab でシートからコンテンツへ進んでも、シートは開いたままです。Escape、「戻る」、ドラッグでは何も起きません。
- モーダルを開く。 シートが末尾側の端から滑り込み、ウィンドウの残りの部分にスクリムがフェードインします。フォーカスはスタンダードと同じように中へ移り、そのままとどまります。最後のフォーカスできる要素で Tab を押すと最初へ、最初の要素で Shift+Tab を押すと最後へ移ります。背後のものはすべて操作不能になります。最初から開いた状態で表示したモーダルのシートも、フォーカスを中へ移します。背後の要素はもうフォーカスを受け取れないからです。
- 閉じる。 シートは末尾側の端へ滑り出て、モーダルならスクリムが消えます。フォーカスが中にあったなら、開いたときにフォーカスを持っていた要素へ戻ります。
- モーダルをドラッグする。 指がプラットフォームのタッチスロップを超えるとシートが追従します。末尾側の端の方へ幅の半分を超えて離すか、その向きにフリングすると、理由
dragで閉じるよう求めます。それより短く、ゆっくりしたドラッグなら元の位置へ滑って戻ります。フリングとタッチスロップの基準はボトムシートと同じです。 - 重ねる。 モーダルのサイドシートから開いたダイアログ、メニュー、別のモーダルのシートは、その上に重なります。Escape で閉じるのは一番上のものだけです。
- 端からのスワイプでは開かない。 シートはボタンから開き、画面の端からのスワイプで開くことはありません。
- 動きを減らす設定では、スタンダードのシートは瞬時に開閉し、モーダルのシートは滑らせずにフェードします。
- 「絞り込み」「詳細」「プロパティ」のように、中身を表す短い見出しを付けます。
- 絞り込みボタンの切り替えなど、利用者側のコントロールで閉じるのでない限り、スタンダードのシートの閉じるボタン(既定で描かれます)を残します。
- スタンダードとモーダルで同じコンテンツを使い、ウィンドウの幅だけで切り替えられるようにします。
- すべてのアクションに
confirmingかdismissiveの印を付け、置き場所はシートに任せます。 - モーダルのシートには、閉じる手段だけでなく、適用のように作業を終えるアクションを用意します。
- スタンダードが収まる幅のウィンドウでモーダルのサイドシートを使わない。
- プロダクトのナビゲーションをサイドシートに置かない。ナビゲーションドロワーを使います。
- 閉じるボタンや actions スロットのボタンの見た目を
variant以上に変えない。アクションを手で並べ替えない。 - 人が求めていないのにサイドシートを開かない。
- ロール
dialog- ラベル付け
- The headline is the sheet's accessible name and is exposed as a heading: the modal dialog's name, or the standard sheet's complementary region's. The close button is named by closeLabel. Each action is named by its own label; confirming and dismissive are not announced, and the actions are read in the order they are placed (BGR-15). The dividers are decorative.
- 最小ターゲット
- 見た目のサイズが小さくても、ヒット領域は最小ターゲットサイズを満たします。 minimum 32px · pointer 44px · spacing 8px · touch 48px
キーボード操作
| キー | 動作 |
|---|---|
| Tab / Shift+Tab | Moves focus through the close button, the content's focusable elements and the actions, in that order, the actions in the order they are placed (BGR-15). In an open modal sheet it wraps and never leaves the sheet; from an open standard sheet it moves on to the content, and the sheet stays open. |
| Enter / Space | Activates the focused close button (close, reason close) or action (its click; the sheet does not close by itself, SSH-18). The sheet adds no default action to Enter. |
| Escape | Asks an open modal sheet to close (close, reason escape); with stacked overlays, only the topmost. A standard sheet ignores it. |
備考
- The sheet has no target of its own; its close button and its actions are Tanrei Buttons, which meet the minimum target themselves (BTN-5). comp.side-sheet.actions.gap keeps each action's whole target in a column in the default system.
- A modal sheet offers at least one action (SSH-16): it holds focus until the person is done, so a person using a screen reader or a switch finds a way to finish the task where they finish reading it, not only a way out. The close button, Escape, back and the scrim still dismiss it. A standard sheet sits beside the content, which the person keeps working in, so it may have no action.
- Placement is by task, not by code order: in a row the dismissive action is at the start and the confirming one at the end; in a column the confirming one is on top. Focus and screen readers follow the placed order.
- Dragging is never the only way (WCAG 2.5.7): a modal sheet also closes with its close button, Escape, back or a press on the scrim.
- The standard form is a layout region, not an overlay: opening it moves focus in and closing it returns focus, and focus leaving it never closes it. The modal form is a modal overlay. The web presents it with a modal <dialog> element (showModal gives inertness and the top layer; its cancel event is cancelled and becomes close with the reason escape); Compose draws it in a window of its own (a Dialog, as TanreiDialog and the modal drawer are) and Flutter as a controlled overlay in a Stack over the page, each with a scrim, trapping focus and answering back; SwiftUI presents it as the dialog is, with a full-screen cover on iOS; React Native presents it in a transparent Modal.
- Opening a modal sheet by a swipe from the screen edge is left out, as for the drawer: the product opens it from a button.
生成元 specs/side-sheet.spec.json, tokens/sys/measure.tokens.json
- モーダルのシートは、見出しを名前とするモーダルなダイアログとして伝えます。スタンダードのシートは、見出しを名前とする補足の領域(Web では aside のランドマーク)です。
- 見出しは見出しとして伝えます。区切り線は支援技術から隠します。
- 開くと、指定した要素かシートへフォーカスを移します。閉じると、開いたときにフォーカスを持っていた要素へ戻します。どちらの形でも同じです。
- モーダルのシートでは、Tab と Shift+Tab は中で折り返し、背後のものはすべて操作不能にして支援技術から隠します。Escape、「戻る」、スクリムの押下で閉じるよう求めます。オーバーレイが重なっているときは、一番上のものだけです。
- スタンダードのシートからは Tab でコンテンツへ進み、シートは開いたままです。Escape と「戻る」は無視します。
- 閉じるボタンは
closeLabelを名前とする Tanrei のアイコンだけのボタン、アクションは Tanrei のボタンで、それぞれが最小ターゲットを満たします。縦に積んだアクションの間の 16 で、どのターゲットも欠けません。 - フォーカスとスクリーンリーダーは、目で見るのと同じ、置かれた順番でアクションをたどります。アクションはオンスクリーンキーボードの上に留まるので、シートの中で入力している間も届きます。
- Enter と Space はフォーカス中の閉じるボタンかアクションを実行します。シートが Enter に既定のアクションを割り当てることはありません。
- ドラッグだけが閉じる手段になることはありません。閉じるボタン、Escape、「戻る」、スクリムでも閉じられます。
- 文字を大きくしても、シートの幅は変わりません。ヘッダーは見出しに合わせて高くなり、コンテンツはスクロールします。
- 見出しのないシート、
closeLabelのない閉じるボタン、アクションなしで開いたモーダルのシート、デザインシステムにないバリアントは、デバッグビルドで報告します。
プラットフォームごとの既知の制約:
- SwiftUI、Compose、Flutter、React Native:
dividerVisibilityは、これらのプラットフォームの追従を待っています。これらはscrollと同じく、コンテンツがスクロールしている間だけヘッダーの下に区切り線を引きます。 - Web:Firefox では、ダイアログと同じく、モーダルのシートは滑り出さずにすぐ消えます。タッチでのドラッグと実際のセーフエリアのインセットは、テストで確かめていません。
- SwiftUI には、ダイアログのロールも補足領域のロールもありません。シートは見出しを名前とするコンテナで、モーダルの間はモーダルのトレイトを持ちます。SwiftUI はシートそのものにキーボードのフォーカスを渡せないため、印がないときは VoiceOver が見出しへ移り、キーボードのフォーカスは元の場所に残ります。閉じてもキーボードのフォーカスはどこにも戻りません。iOS では、モーダルのカバーが VoiceOver のフォーカスを戻し、スタンダードのシートは手放します。スタンダードのシートは滑る間に切り取られないので、ウィンドウの行の後端に置いてください。macOS では、モーダルのシートはモディファイアーを付けたビューしか覆わないので、ウィンドウのルートに付けてください。
- SwiftUI:コンテンツの素のテキストは本文のフォントになりますが行の高さは付かないため、ほかのプラットフォームとまったく同じ位置には置かれません。Escape と iOS の「戻る」ジェスチャーはつないでありますが、押して確かめるテストはありません。
- Compose:モーダルのシートが閉じるとき、そのウィンドウは退場の動きが終わるまでフォーカスを持ったままです。追従するまでは、アクションは
RowScopeの中で呼ばれますが、並べ方はシートが決めるため、weightや配置の指定は効きません。ドラッグは Robolectric でだけ確かめており、実機では試していません。 - Flutter:スタンダードのシートは名前付きのコンテナで、補足領域のランドマークではありません。Flutter 3.32 にそのロールがないためです。実機の TalkBack と VoiceOver では、まだ確かめていません。
- React Native は、どの要素がフォーカスを持っていたかを知ることができません。そのため、シートが閉じるとフォーカスは開いた要素ではなく、プラットフォームが決める場所へ移ります。Modal、ドラッグ、幅の動き、インセットは Jest でだけ確かめており、実機では試していません。
m3-likeでは、シートの幅と、スタンダードの形の細い境界線は既定のシステムの値のままです。Material の値は、忠実度の参照データがサイドシートを扱うようになるまで保留しています。
- すべてのシートに
headlineを付けます。それがシートの名前です。 - 既定で描かれる閉じるボタンには、「絞り込みを閉じる」のように何を閉じるのかが分かる
closeLabelを付けます。 - シートは利用者側で閉じます。
closeを受けたとき(Web ではcloseイベント、Compose、Flutter、React Native ではonClose)にopenを false にします。SwiftUI ではシートがバインディングに false を書き込みます。 - スタンダードのシートには閉じる手段を用意します。閉じるボタンか、ラベルを持つ利用者側のコントロールです。
- モーダルのシートには少なくとも 1 つのアクションを用意し、それぞれに
confirmingかdismissiveの印を付け、そのハンドラーからシートを閉じます。 - シートは、自分のラベルを持つ目に見えるボタンから開きます。
- 最初の絞り込み条件など、役に立つときは最初のフォーカスを指定します。