ボトムシート
補足のコンテンツや短い作業を、画面の下端からせり上げて表示します。残りを使える状態のまま画面の下部に重ねるか、モーダルな手順として閉じるまでほかのすべてを暗くしてふさぎます。
- カテゴリ
- コンテインメント
- トークン
comp.bottom-sheet- レベル
- 合成
- 構成要素
- Modal layer
- Web
tanrei-bottom-sheetschedule - SwiftUI
tanreiBottomSheetschedule - Jetpack Compose
TanreiBottomSheetschedule - Flutter
TanreiBottomSheetschedule - React Native
BottomSheetschedule
生成元 docs/status/status.json, specs/bottom-sheet.spec.json
モーダルのシート:ボタンで開きます
写真の保存先を選んでください。あとから設定で変更できます。
ボトムシートは制御コンポーネントで、自分で閉じることはありません。open を設定して開きます。人が閉じようとすると、シートはその理由を添えて close を発火するので、それを受けて利用者側で open を false に戻します。例外は SwiftUI だけです。SwiftUI では open がバインディングなので、シートが自分で false を書き込み、そのあとで onClose を呼びます。open を true のままにしておけばシートは閉じず、ドラッグのあとなら一番低い高さに戻ります。Web ではダイアログと動きが違う点に注意してください。シートはネイティブの cancel イベントをキャンセルして代わりに close を発火するので、Escape を押しても、利用者側で閉じるまでは閉じません。
close が伝える理由は次の 5 つです(表記はプラットフォームの流儀に従います)。
| 理由 | 人がしたこと | 対象 |
|---|---|---|
close |
シートの閉じるボタン(closable)を押した |
スタンダードとモーダル |
drag |
一番低い高さの半分より下でドラッグを離した、またはそこから下へフリングした | スタンダードとモーダル |
scrim |
モーダルのシートのまわりの暗い部分を押した | モーダル |
escape |
Escape を押した | モーダル |
back |
プラットフォームの「戻る」を使った(Android の戻るボタンやジェスチャー、iOS のアクセシビリティの「戻る」ジェスチャー)。Web では、ブラウザーでの Android の戻るジェスチャーのように、キーを伴わない閉じる要求が back になります。 |
モーダル |
close を発火するのは dismissible が true の間だけで、1 回の求めにつき 1 回だけです。false の間は、スクリム、Escape、「戻る」では何も起きず、下へドラッグしても一番低い高さに戻るだけです。例外は閉じるボタンで、dismissible にかかわらず理由 close で閉じるよう求めます。利用者側があえて描くことにした出口だからです。
シートには variant で選ぶ 2 つの形があります。既定のモーダルはモーダルなオーバーレイです。ウィンドウ全体を暗くし、フォーカスを中にとどめ、閉じたらフォーカスを返します。スタンダードはレイアウト領域で、オーバーレイではありません。置いた親要素の下部に重なりますが、スクリムは描かず、上のコンテンツはそのまま使えます。
シートに入れるものは利用者側で決めます。シートはコンテンツに余白を付け(行が自分で余白を持つリストなら inset を false にします)、はみ出しを切り取り、コンテンツの色を渡し、スクロールさせます。headline を渡すと、コンテンツの上にヘッダーを描きます。ヘッダーには見出し(見出しの要素で、label が空のときはシートの名前にもなります)と、closable をオンにしたときは closeLabel を名前とする閉じるボタンが並びます。閉じるボタンは小さいボタンで、Tanrei のほかの閉じるボタンと同じく、既定のシステムでは Tonal です。ヘッダーの下には dividerVisibility に従って区切り線が引かれ、ヘッダーはコンテンツがスクロールしても動きません。ハンドルと、モーダルのシートではスクリムがシートの出口なので、closable は既定でオフです。スタンダードのシートやハンドルのないシートではオンにしてください。
開いたときにフォーカスを受け取る要素は、プラットフォームの印で指定します。Web では autofocus、SwiftUI では .defaultFocus か @AccessibilityFocusState、Compose では initialFocus に渡す FocusRequester、Flutter では autofocus: true、React Native では initialFocusRef です。指定しなければフォーカスはシートそのものへ移り、ラベルが最初に読み上げられます。
<tanrei-button id="save" variant="outlined">写真を保存</tanrei-button>
<!-- 既定はモーダルです。closedby="none" は dismissible を false にします。 -->
<tanrei-bottom-sheet id="save-sheet" label="写真を保存" handle-label="シートの高さを変更">
<p>写真の保存先を選んでください。</p>
<tanrei-button variant="text" autofocus>完了</tanrei-button>
</tanrei-bottom-sheet>
<!-- スタンダード:包含ブロックの下部に重なります。flush は inset を false に、no-handle は handle を false にします。 -->
<div class="map" style="position: relative">
<tanrei-bottom-sheet variant="standard" open label="近くの場所" handle-label="シートの高さを変更" flush>
<tanrei-list>…</tanrei-list>
</tanrei-bottom-sheet>
</div>
<script type="module">
import '@tanrei/web/components/bottom-sheet';
const sheet = document.querySelector('#save-sheet');
document.querySelector('#save').addEventListener('click', () => (sheet.open = true));
sheet.querySelector('tanrei-button').addEventListener('click', () => (sheet.open = false));
// シートは自分では閉じません。detail.reason は drag、scrim、escape、back のどれかです。
sheet.addEventListener('close', (e) => (sheet.open = false));
// 人がもう一方の高さへ動かしました。detent はシートが更新済みです。
sheet.addEventListener('detentchange', (e) => console.log(e.detail.detent));
</script>import { BottomSheet, Button } from '@tanrei/react';
<BottomSheet
open={open}
label="写真を保存"
handleLabel="シートの高さを変更"
onClose={() => setOpen(false)}
onDetentChange={(e) => setDetent(e.detail.detent)}
>
<p>写真の保存先を選んでください。</p>
<Button variant="text" onClick={() => setOpen(false)}>完了</Button>
</BottomSheet>import Tanrei
// .sheet と同じく、覆うビューに付けるモディファイアーです。閉じるとバインディングに false が書き込まれ、
// そのあとで理由を添えて onClose が呼ばれます。人が選んだ高さは detent に書き込まれます。
PhotoView(photo)
.tanreiBottomSheet(isPresented: $saving, detent: $detent,
handleLabel: "シートの高さを変更", label: "写真を保存") {
SaveTargets(photo)
TanreiButton("完了", variant: .text) { saving = false }
}
// スタンダード:覆うビューの下部に重ねるオーバーレイです。
MapView()
.tanreiBottomSheet(isPresented: $placesShown, variant: .standard,
handleLabel: "シートの高さを変更", label: "近くの場所") {
PlacesList()
}import io.github.chnotchy.tanrei.components.TanreiBottomSheet
import io.github.chnotchy.tanrei.components.TanreiButton
import io.github.chnotchy.tanrei.tokens.TanreiButtonVariant
// 閉じるときの動きを再生できるよう、開いている間だけ呼ぶのではなく visible で切り替えます。
// 理由を受け取る onClose で visible を false にします。
TanreiBottomSheet(
visible = saving,
onClose = { saving = false },
detent = detent,
onDetentChange = { detent = it },
handleLabel = "シートの高さを変更",
contentDescription = "写真を保存",
) {
SaveTargets(photo)
TanreiButton(text = "完了", onClick = { saving = false }, variant = TanreiButtonVariant.Text)
}import 'package:tanrei/tanrei.dart';
// モーダルのシートは、ページ全体を覆う Stack の最後の子として置きます。onClose を受けて open を false にします。
Stack(
children: <Widget>[
const Positioned.fill(child: PhotoPage()),
TanreiBottomSheet(
open: saving,
onClose: (TanreiSheetCloseReason reason) => setState(() => saving = false),
detent: detent,
onDetentChanged: (value) => setState(() => detent = value),
handleSemanticLabel: 'シートの高さを変更',
semanticLabel: '写真を保存',
child: const SaveTargets(),
),
],
),import { useRef, useState } from 'react';
import type { View } from 'react-native';
import { BottomSheet, Button } from '@tanrei/react-native';
const done = useRef<View>(null);
const [open, setOpen] = useState(false);
// 制御コンポーネントです。理由('drag'、'scrim'、'escape'、'back')を受け取る onClose で open を false にします。
// モーダルのシートは透明な Modal で表示します。シート自身の insets プロパティは、指定した辺だけ TanreiProvider の insets を置き換えます。
<BottomSheet
open={open}
onClose={(reason) => setOpen(false)}
detent={detent}
onDetentChange={setDetent}
handleLabel="シートの高さを変更"
accessibilityLabel="写真を保存"
initialFocusRef={done}
>
<SaveTargets />
<Button ref={done} variant="text" onPress={() => setOpen(false)}>完了</Button>
</BottomSheet>親要素の下部に重なるスタンダードのシート:下へドラッグすると閉じます
構成要素
| パーツ | 説明 |
|---|---|
1 scrim · 任意 | The modal layer's scrim over everything behind a modal sheet (MDL-1). It blocks presses and, while the sheet is dismissible, a press on it asks the sheet to close. A standard sheet draws none. |
2 container | The raised surface docked to the bottom edge of the area it covers, centred and no wider than its maximum width, its top corners rounded and its bottom corners square. Its color reaches under the bottom system inset. |
3 drag-handle · 任意 | The control at the top of the sheet: a Handle without a value, always drawn, that carries hover, focus and press on its box, with the grip centred in it. Activating it moves the sheet between its two heights. |
4 drag-handle.grip · 任意 | The Handle's line: the short rounded bar drawn in the handle's box that says the sheet can be dragged. |
5 header · 任意 | The row below the drag handle's row (or at the top without one) that holds the headline and the close button. Drawn while headline is not empty or closable is true; it stays in place while the content scrolls. |
6 headline · 任意 | What the sheet is about ("Share photo"), a heading. It names the sheet where label is empty. |
7 close-button · 任意 | An icon-only Button of the small size showing the close glyph at the end of the header, in the variant comp.bottom-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. |
8 divider · 任意 | A hairline across the sheet 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 content | Whatever the product puts in the sheet: a list of choices, a form, details. The sheet insets it (unless inset is false), clips it, hands it a content color and scrolls it; it does not lay it out. |
10 focus-ring · 任意 | Gap-then-ring indicator around the drag handle's box, shown for keyboard focus. |
プロパティ
| プロパティ | 型 | 既定値 | 説明 |
|---|---|---|---|
variant | standard | modal | "modal" | Standard sits over the lower part of the content while the content above stays usable (a layout region); modal dims and blocks everything else and holds focus until it closes (a modal overlay). Both read the same container tokens: the scrim and the behaviour set them apart. Not a token axis. |
open | boolean | false | Whether the sheet is shown. The product opens it, and closes it in answer to close (or from its own controls); on SwiftUI, where it is a binding, the sheet writes false itself. |
detent | partial | full | "partial" | The height the sheet rests at while its content is taller than it: partial takes comp.bottom-sheet.container.partial-fraction of the area; full reaches up to comp.bottom-sheet.container.margin-block-start below the area's top. A sheet whose content is shorter than its partial height has one height, and detent changes nothing. The person moves between the two by dragging or with the drag handle (detentChange). |
dismissible | boolean | true | Whether a drag below the sheet's lowest height asks it to close and, for a modal sheet, whether Escape, the platform's back and a press on the scrim do too. Set it to false only when the person must finish the sheet's task with one of its own controls. |
handle | boolean | true | Draws the drag handle at the top of the sheet. Without it the sheet can still be dragged by its padding and content (BSH-9), but a keyboard or switch user has no way to change its height, so leave it on for a sheet whose content can be taller than its partial height. |
handleLabel | string | "" | The drag handle's accessible name ("Resize sheet"); its expanded state is exposed with it. Required while the handle is interactive (BSH-11). |
inset | boolean | true | Insets the content by comp.bottom-sheet.container.padding. Set it to false for content that pads itself or reaches the edges (a list whose rows carry their own padding). |
label | string | "" | Names the sheet for assistive technology ("Share photo"): the modal dialog, or the standard sheet's region. Required unless headline names it. |
headline | string | "" | What the sheet is about ("Share photo"), drawn in its header (BSH-28). While label is empty it is the sheet's accessible name. |
closable | boolean | false | Draws the close button at the end of the header (BSH-29). False by default: the drag handle and, for a modal sheet, the scrim are the sheet's ways out. Set it where a person who cannot drag needs one they can see — a standard sheet, or a sheet without a handle. |
closeLabel | string | "" | The close button's accessible name ("Close sharing"). Required while closable is true. |
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. scroll: only while the content is scrolled away from its top. auto takes comp.bottom-sheet.divider.visibility, the design system's answer (always in the default system). |
スロット
| スロット | 説明 |
|---|---|
default | The content. A fixture's content string is plain text, which takes sys.typescale.body in the content color (BSH-8). |
イベント
| イベント | 説明 |
|---|---|
close | The sheet asks to be closed: its close button (BSH-29), a released drag below its lowest height, or for a modal sheet a press on the scrim, Escape or the platform's back. Carries which of these it was, named close, drag, scrim, escape or back (in the platform's casing). The close button asks whatever dismissible says; the others only while dismissible is true. |
detentChange | The person moved the sheet to its other height, by a drag or with the drag handle. Carries the new detent (partial or full). Not emitted when the product sets detent. |
状態
振る舞い
EARS 記法による要件です。各プラットフォームの実装は、この ID に対応するテストで検証されます。要件の原文は仕様ファイルのものをそのまま掲載しています。
| ID | 要件 |
|---|---|
BSH-1 | WHEN open becomes true THE bottom sheet SHALL slide its container up from below the bottom edge of the area it covers to its height (BSH-6) — WHILE variant is modal over the whole window on its modal layer, fading the layer's scrim in comp.bottom-sheet.scrim.color and comp.bottom-sheet.scrim.blur in behind it in step (MDL-1, MDL-7); WHILE variant is standard over the bottom of the parent the product placed it in, with no scrim — over comp.bottom-sheet.motion.enter-duration with comp.bottom-sheet.motion.enter-easing; WHEN open becomes false THE bottom sheet SHALL slide it down out of view from wherever it is and fade the scrim out, over comp.bottom-sheet.motion.exit-duration with comp.bottom-sheet.motion.exit-easing. WHERE motion is reduced it SHALL fade in and out in place without sliding, or appear and disappear at once on a platform whose reduced-motion setting removes every animation (Android's). A sheet that is mounted with open true SHALL appear in place without motion. |
BSH-2 | WHILE variant is modal and the sheet is open THE bottom sheet SHALL make everything outside it inert, keep the content behind it from scrolling and keep focus inside it, as its modal layer does (MDL-2, MDL-4): a modal overlay. |
BSH-3 | WHEN open changes from false to true THE bottom 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 its label first; WHEN it closes while focus is inside it THE bottom 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 for a closed overlay. 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. |
BSH-4 | WHILE variant is standard THE bottom sheet SHALL draw no scrim, SHALL NOT make anything inert, SHALL NOT keep focus inside it, SHALL let Tab and Shift+Tab move focus between it and the content it covers without closing it, and SHALL ignore Escape and the platform's back: a standard sheet is a layout region, not an overlay. The product places it over the content it covers, in a parent that holds both; the sheet SHALL dock to that parent's bottom edge, SHALL NOT resize or pad the content, and SHALL let presses on the part of the parent above it reach the content. |
BSH-5 | WHILE dismissible is true, WHEN the user presses the scrim, presses Escape or uses the platform's back WHILE variant is modal, as its modal layer answers them (MDL-6), or releases a drag that asks to close (BSH-9) in either variant, THE bottom sheet SHALL emit close exactly once with the reason scrim, escape, back or drag, and SHALL then 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). WHILE dismissible is false THE bottom sheet SHALL ignore these inputs and emit nothing for them (its close button, BSH-29, still asks); a press on the scrim SHALL still reach nothing behind it. |
BSH-6 | THE bottom sheet SHALL be as tall as its drag handle's row, its header (BSH-28) and its content (with its insets), but no taller than its detent allows: WHILE detent is partial, comp.bottom-sheet.container.partial-fraction of the height of the area it covers; WHILE detent is full, that height less comp.bottom-sheet.container.margin-block-start. The area is the window below its top system inset for a modal sheet, and the parent the product placed it in for a standard sheet; WHILE the on-screen keyboard is shown the area SHALL end at the keyboard's top edge, so that the sheet sits on the keyboard and both heights are taken from the smaller area. The sheet has two heights only WHILE its content is taller than its partial height; otherwise it has one, and detent changes nothing. |
BSH-7 | THE bottom sheet SHALL draw its container at the bottom edge of the area, as wide as the area but no wider than comp.bottom-sheet.container.max-width and centred in it, filled with comp.bottom-sheet.container.color, raised by comp.bottom-sheet.container.elevation, with its top corners rounded by comp.bottom-sheet.container.shape and its bottom corners square; it SHALL extend its color under the bottom system inset and keep its content above that inset, and WHILE the on-screen keyboard is shown it SHALL sit on the keyboard (BSH-6) with its content above it. |
BSH-8 | THE bottom sheet SHALL lay out, top to bottom, the drag handle's row WHILE handle is true (BSH-12), the header WHILE there is one (BSH-28) and then its content. WHILE inset is true it SHALL inset the content by comp.bottom-sheet.container.padding from its start, end and bottom edges, and from its top edge WHILE handle is false and no header is drawn; WHILE inset is false the content SHALL reach the container's edges, below the handle's row and the header. It SHALL clip the content to the container's shape, SHALL hand it comp.bottom-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), and plain text given as content, where the platform must style it, SHALL take sys.typescale.body. |
BSH-9 | WHEN the user drags the sheet vertically — by its handle's row, by the padding around its content, or by its content where BSH-10 lets the drag reach the sheet — beyond the platform's touch slop THE bottom sheet SHALL follow the finger or pointer, never above its full height; WHEN the drag is released it SHALL settle at the next height in the direction of a fling, or else at the height nearest to where it was released, over comp.bottom-sheet.motion.enter-duration with comp.bottom-sheet.motion.enter-easing (at once where motion is reduced). A release below half of the sheet's lowest height, or a downward fling from its lowest height, SHALL ask to close WHILE dismissible is true (BSH-5, BSH-20); WHILE dismissible is false the sheet SHALL settle at its lowest height instead. A fling is a release faster than the platform's minimum fling velocity (Android's ViewConfiguration.getScaledMinimumFlingVelocity, Flutter's kMinFlingVelocity) or, where the platform names none (the web, SwiftUI, React Native), 0.5 points per millisecond; the touch slop is the platform's own (Android's ViewConfiguration.getScaledTouchSlop, Flutter's kTouchSlop) or, where the platform names none, 8 points. A release's velocity is the pointer's over its last moves: WHERE the pointer has not moved for longer than the time after which the platform's velocity tracker takes a pointer as stopped (40 milliseconds in Android's VelocityTracker, Jetpack Compose's VelocityTracker and Flutter's VelocityTracker) or, where the platform names none (the web, SwiftUI, React Native), 40 milliseconds before the release THE bottom sheet SHALL take the release as having no velocity, so that a drag that stops where it is wanted and is let go there is no fling, however fast it moved before it stopped. WHEN a drag ends at the height it did not start from THE bottom sheet SHALL set detent to it and emit detentChange exactly once with it. |
BSH-10 | WHILE the content is taller than the room the sheet gives it THE bottom sheet SHALL scroll the content inside the container, below the handle's row, and SHALL let assistive technology and keyboard focus scroll it at either height; a drag upward on the content SHALL first raise the sheet to its full height and then scroll the content, and a drag downward on the content SHALL first scroll the content to its top and then lower the sheet. |
BSH-11 | WHILE handle is true and the sheet has two heights THE bottom sheet SHALL make the drag handle one focus stop, a button named by handleLabel with the expanded state, true WHILE detent is full (its label and expanded, HDL-8), and WHEN the user activates it by pointer, touch, Enter or Space (its activate, HDL-7) THE bottom sheet SHALL move to its other height as BSH-9 settles and emit detentChange exactly once with the new detent; WHILE the sheet has one height the handle SHALL still be drawn at rest but SHALL NOT be focusable, pressable or exposed to assistive technology. WHILE the handle is interactive and handleLabel is empty it SHALL still render, and in debug builds report that the handle has no accessible name. |
BSH-12 | WHILE handle is true THE bottom sheet SHALL draw at its top a row holding the drag handle — a Handle of axis vertical and size medium, with no value and revealed — centred across the sheet with comp.bottom-sheet.handle.margin-block above and below it, and SHALL give that Handle its own tokens in place of the Handle's: the box comp.bottom-sheet.handle.container-width wide and comp.bottom-sheet.handle.container-height high with comp.bottom-sheet.handle.container-shape corners (HDL-3), the grip — the Handle's line — comp.bottom-sheet.handle.grip-width long and comp.bottom-sheet.handle.grip-height thick (HDL-1), and the container, line and state-layer colors of each state from the drag-handle control (comp.bottom-sheet.drag-handle.container.color, comp.bottom-sheet.drag-handle.grip.color, comp.bottom-sheet.drag-handle.state-layer.color, and so on for each state), its opacity in each state, and comp.bottom-sheet.motion.state-duration, comp.bottom-sheet.motion.state-easing and comp.bottom-sheet.motion.feedback-duration as its motion; in the default system each of them aliases the Handle's own. The row SHALL stay in place while the content scrolls. |
BSH-13 | WHILE the drag handle is hovered, keyboard-focused or pressed THE bottom sheet SHALL render the handle's box, grip and opacity from the component tokens of the highest-precedence active state, with precedence pressed over focused over hovered, and the grip at the Handle's length and thickness of a handle in use (HDL-4); focus gained by pointer or touch SHALL NOT select the focused state. A drag that starts on the handle SHALL keep it pressed while the sheet follows the drag, as a Handle is held for as long as a drag lasts (HDL-11). |
BSH-14 | WHEN the drag handle receives keyboard focus THE bottom sheet SHALL show the focus ring around the handle's box without animation; focus gained by pointer or touch SHALL NOT show it. |
BSH-15 | THE bottom sheet SHALL give the drag handle the Handle's hit area (HDL-5): at least sys.target.touch wide and high for touch input and sys.target.pointer for fine pointers, centred on its box, without changing its drawn size, and the Handle's vertical resize cursor; a platform that cannot tell the input type SHALL use sys.target.touch. A press on the rest of the handle's row drags the sheet as BSH-9 says. |
BSH-16 | WHEN the drag handle is pressed by pointer, touch, Enter or Space THE drag handle SHALL show the pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner (HDL-11). |
BSH-17 | WHILE pressed THE drag handle SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole control and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; grip and opacity still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.bottom-sheet.motion.feedback-duration with comp.bottom-sheet.motion.state-easing, fades over comp.bottom-sheet.motion.state-duration with comp.bottom-sheet.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced. |
BSH-18 | THE bottom sheet SHALL ease its container's top corners and the drag handle's box corners 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 show the handle's keyboard focus as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README. |
BSH-19 | WHEN the drag handle's colors, or its grip's length and thickness, change between states THE bottom sheet SHALL animate them over comp.bottom-sheet.motion.state-duration with comp.bottom-sheet.motion.state-easing, and SHALL change them at once where motion is reduced. |
BSH-20 | WHEN a released drag asks to close THE bottom sheet SHALL emit close (BSH-5), hold where the drag left it for one frame, and then slide out from there as BSH-1 says IF open has become false, or settle back at its lowest height as BSH-9 says IF open is still true (the product declined to close it). |
BSH-21 | WHEN the product changes detent while the sheet is open THE bottom sheet SHALL move to the new height over comp.bottom-sheet.motion.enter-duration with comp.bottom-sheet.motion.enter-easing (at once where motion is reduced) and SHALL NOT emit detentChange; where detent is a plain value (web, Compose, Flutter, React Native), a height the person chose (BSH-9, BSH-11) SHALL stay until the product sets detent again, and where it is a binding (SwiftUI) the sheet SHALL write the new detent to it. |
BSH-22 | THE bottom sheet SHALL expose its container, WHILE variant is modal, as a modal dialog named by label and, WHILE variant is standard, as a region named by label (a landmark on the web) — by the headline WHILE label is empty (BSH-28); WHILE label and headline are both empty it SHALL still render, and in debug builds report that it has no accessible name. The drag handle's grip, the Handle's line, SHALL be hidden from assistive technology; the handle itself is exposed only as BSH-11 says. |
BSH-23 | WHEN a dialog, a menu or another modal sheet opens from a modal bottom sheet THE bottom 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. |
BSH-24 | WHILE the text size is enlarged THE bottom sheet SHALL grow with its content up to the height its detent allows and scroll the content beyond it (BSH-10), grow its header with a headline that wraps, and SHALL NOT scale the drag handle, nor the close button beyond what the Button itself does. |
BSH-25 | WHEN variant or detent names a value the bottom sheet does not define THE bottom sheet SHALL render with the default value, and in debug builds report the unknown value. |
BSH-26 | WHEN the layout direction is right-to-left THE bottom sheet SHALL keep its container and its drag handle centred, and its content SHALL follow the layout direction. |
BSH-27 | THE bottom 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 bottom sheet is shown THE bottom sheet SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state, its height and its focus. |
BSH-28 | WHILE headline is not empty or closable is true THE bottom sheet SHALL draw a header below the drag handle's row (or at the top of the container without one): a row at least comp.bottom-sheet.header.height high holding the headline from comp.bottom-sheet.container.padding after the start edge, in comp.bottom-sheet.headline.typescale and comp.bottom-sheet.headline.color, wrapping onto more lines rather than truncating and growing the row with it, exposed as a heading, and, WHILE closable is true, the close button ending comp.bottom-sheet.header.padding-inline-end before the end edge and comp.bottom-sheet.header.gap after the headline, both centred in the row; below it, a divider comp.bottom-sheet.divider.width thick in comp.bottom-sheet.divider.color across the sheet's full width WHILE dividerVisibility (or, for auto, comp.bottom-sheet.divider.visibility) is always, or is scroll and the content is scrolled away from its top, shown and hidden at once. The header SHALL NOT scroll with the content, and a drag on it SHALL move the sheet as a drag on the handle's row does (BSH-9). WHILE headline is not empty it SHALL name the sheet where label is empty. A dividerVisibility the sheet does not define SHALL be drawn as auto, and reported in debug builds. |
BSH-29 | WHILE closable is true THE bottom sheet SHALL draw its close button as an icon-only Button of the small size showing the glyph close of the system's icon set and named by closeLabel, in the variant comp.bottom-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 bottom sheet SHALL emit close exactly once with the reason close, in either variant and whatever dismissible says, and SHALL then be closed as BSH-5 says. 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. THE bottom sheet SHALL draw no close mark of its own. |
BSH-30 | WHILE handle is false, dismissible is true and closable is false THE bottom sheet SHALL still render, and in debug builds report that it shows no way out a person can see: a drag, Escape and the scrim still close it, but nothing on screen says so to a person who cannot drag. |
BSH-31 | 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 bottom 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 (BSH-22) — its dialog WHILE variant is modal, its 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 label or headline is not empty it is the container's name (BSH-22), over a name given so; WHILE both are 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 bottom sheet SHALL take it off that element. |
プラットフォーム別の名前対応
パリティは API が同一であることを意味しません。同じ契約を、各プラットフォームの流儀で表現します。
| 仕様 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| コンポーネント | tanrei-bottom-sheet | tanreiBottomSheet | TanreiBottomSheet | TanreiBottomSheet | BottomSheet |
variant · プロパティ | variant | variant | variant | variant | variant |
open · プロパティ | open | isPresented | visible | open | open |
detent · プロパティ | detent | detent | detent | detent | detent |
dismissible · プロパティ | closedby | dismissible | dismissible | dismissible | dismissible |
handle · プロパティ | handle | handle | handle | showHandle | handle |
handleLabel · プロパティ | handleLabel | handleLabel | handleLabel | handleSemanticLabel | handleLabel |
inset · プロパティ | inset | inset | inset | inset | inset |
label · プロパティ | label | label | contentDescription | semanticLabel | accessibilityLabel |
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 |
detentChange · イベント | detentchange | detent | onDetentChange | onDetentChanged | onDetentChange |
| 備考 | dismissible is HTML's own closedby attribute, as on <tanrei-dialog>: any (the default) or none. handle and inset are boolean properties that default to true; their attributes are the inverses no-handle and flush (an HTML boolean attribute cannot default to true). handleLabel is the handle-label attribute, closeLabel the close-label attribute and dividerVisibility the divider-visibility attribute (auto reads --tanrei-comp-bottom-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). 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.bottom-sheet.close-button.variant through the internal channel a Button reads from its host; the headline is an <h2>. close is a CustomEvent whose detail.reason is close, drag, scrim, escape or back (back when a modal sheet's native cancel arrives without an Escape key, as Android's back gesture sends it), and detentchange one whose detail.detent is the new detent; the element sets detent itself before firing it. The product sets open to false from close. The content is the default slot. Drags use pointer events with touch-action: none on the handle's row and the top edge; the content scrolls natively and hands the drag over at its top (BSH-10). Fling 0.5 px/ms, slop 8 px. Insets are env(safe-area-inset-bottom); the on-screen keyboard is read from visualViewport. Snapshot harness: a 400 × 720 viewport with a surface page; the sheet opened by open; the screenshot is the viewport, since a modal sheet and its backdrop are in the top layer. The drag handle is a <tanrei-handle> in the shadow root, its container and line exported as the parts handle and grip; the sheet sets the Handle's custom properties to its own tokens on it (BSH-12), answers its activate, and keeps it inert while the sheet has one height (BSH-11). | A view modifier on the view the sheet covers, as .sheet is: .tanreiBottomSheet(isPresented: Binding<Bool>, variant:, detent: Binding<TanreiBottomSheetDetent>, dismissible:, handle:, handleLabel:, inset:, label:, onClose: ((TanreiSheetCloseReason) -> Void)?, content:). Closing writes false to isPresented and then calls onClose with the reason; a height the person chose is written to detent (detentChange). Modal: on iOS presented with .fullScreenCover, a clear presentation background and the system transition off, as tanreiDialog is (that presentation hides the rest of the app from VoiceOver); Tanrei draws the scrim, the sheet and the drag with a DragGesture (fling from its velocity, 0.5 pt/ms). Not the native .sheet: its dim cannot take the scrim token, and on iPad in a regular width it becomes a centred form, not a bottom sheet. macOS, which has no full-screen cover, draws the modal sheet as an overlay of the modified view with the modal trait. Standard: .overlay(alignment: .bottom) of the modified view. Initial focus is .defaultFocus or an @AccessibilityFocusState the product passes; Escape is .onKeyPress(.escape) and the escape gesture .accessibilityAction(.escape). Snapshot harness: renders TanreiBottomSheetSurface (the sheet and, for a modal sheet, the scrim) in place in a 400 × 720 frame over a surface page, because a snapshot of the presenting view does not include a presented window. Not built on this platform yet: the header — headline, closable, closeLabel and dividerVisibility, the close reason close (BSH-28, BSH-29) — and the report of a sheet with no way out a person can see (BSH-30). | TanreiBottomSheet(visible, onClose: (TanreiSheetCloseReason) -> Unit, variant, detent, onDetentChange, dismissible, handle, handleLabel, inset, contentDescription, initialFocus: FocusRequester?, content). visible rather than calling it only while open, so that the exit motion can run before the sheet goes; the caller sets visible to false from onClose. The sheet keeps a height the person chose until detent changes (BSH-21). Modal: drawn in a Dialog window of its own, as TanreiDialog and the modal drawer are, with usePlatformDefaultWidth = false, decorFitsSystemWindows = false and the window's own dim off (Tanrei draws the scrim); the window traps focus, answers back and hides the content behind from TalkBack; the sheet sets a pane title from contentDescription. Standard: fills its parent (a Box over the content) and draws the sheet at its bottom edge; the part above takes no touches. Drag with Modifier.draggable and nested scroll for BSH-10; fling and slop from ViewConfiguration. Snapshot harness: renders TanreiBottomSheetContent (the sheet and, for a modal sheet, the scrim) in place in a 400 × 720 dp box over a surface page; the window itself is covered by instrumented tests. Not built on this platform yet: the header — headline, closable, closeLabel and dividerVisibility, the close reason close (BSH-28, BSH-29) — and the report of a sheet with no way out a person can see (BSH-30). | TanreiBottomSheet(variant:, open:, onClose: ValueChanged<TanreiSheetCloseReason>, detent:, onDetentChanged:, dismissible:, showHandle:, handleSemanticLabel:, inset:, semanticLabel:, child:), controlled: the caller sets open to false from onClose. The product places it in a Stack over what it covers — for a modal sheet, a Stack over the whole page, as the modal navigation drawer — not showModalBottomSheet, which needs Material. The modal form draws its scrim over the whole Stack, keeps focus in a FocusScope, blocks the page's semantics (BlockSemantics), names itself with Semantics(scopesRoute: true, namesRoute: true), answers back through PopScope and Escape through Shortcuts/Actions. Initial focus is autofocus: true on a focusable widget. Drag with a vertical drag recogniser; fling kMinFlingVelocity, slop kTouchSlop. Snapshot harness: a golden of the whole 400 × 720 surface with the sheet in a Stack over a surface page. Not built on this platform yet: the header — headline, closable, closeLabel and dividerVisibility, the close reason close (BSH-28, BSH-29) — and the report of a sheet with no way out a person can see (BSH-30). | Controlled: open, onClose(reason), detent, onDetentChange; the caller sets open to false from onClose. Modal: presented in a transparent Modal with animationType="none", statusBarTranslucent and navigationBarTranslucent (Tanrei draws the scrim and the motion); onRequestClose (Android back, and Escape on a hardware keyboard) is close with the reason back or escape. Standard: a View positioned absolutely at the bottom of its parent. Drags with PanResponder (no gesture-handler dependency): slop 8 pt, fling 0.5 pt/ms. The on-screen keyboard is read from Keyboard events. Initial focus is an initialFocusRef the product passes, moved with AccessibilityInfo.setAccessibilityFocus. 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 BottomSheetSurface (the sheet and, for a modal sheet, the scrim) in place in a 400 × 720 view over a surface page, since a Modal is a native window the snapshot does not include. React Native cannot hand one gesture from the sheet to the content's native scroll view partway through, so BSH-10's hand-off takes two gestures there; the package takes no gesture-handler dependency (as it takes no safe-area one). Not built on this platform yet: the header — headline, closable, closeLabel and dividerVisibility, the close reason close (BSH-28, BSH-29) — and the report of a sheet with no way out a person can see (BSH-30). |
トークン
| トークン | 参照先 | 解決値(ライト) |
|---|---|---|
--tanrei-comp-bottom-sheet-close-button-variant | — | 1 |
--tanrei-comp-bottom-sheet-container-color | sys.color.surface-container-low | |
--tanrei-comp-bottom-sheet-container-elevation | 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-bottom-sheet-container-margin-block-start | sys.space.space600 | 48px |
--tanrei-comp-bottom-sheet-container-max-width | — | 640px |
--tanrei-comp-bottom-sheet-container-padding | sys.space.space200 | 16px |
--tanrei-comp-bottom-sheet-container-partial-fraction | — | 0.5 |
--tanrei-comp-bottom-sheet-container-shape | sys.shape.corner.large | 12px |
--tanrei-comp-bottom-sheet-content-color | sys.color.on-surface | |
--tanrei-comp-bottom-sheet-divider-color | sys.color.outline-variant | |
--tanrei-comp-bottom-sheet-divider-visibility | sys.behavior.divider-visibility | 0 |
--tanrei-comp-bottom-sheet-divider-width | sys.border.hairline | 1px |
--tanrei-comp-bottom-sheet-drag-handle-container-color | comp.handle.standard.container.color | |
--tanrei-comp-bottom-sheet-drag-handle-focused-container-color | — | |
--tanrei-comp-bottom-sheet-drag-handle-focused-grip-color | — | |
--tanrei-comp-bottom-sheet-drag-handle-focused-state-layer-color | — | |
--tanrei-comp-bottom-sheet-drag-handle-grip-color | comp.handle.standard.line.color | |
--tanrei-comp-bottom-sheet-drag-handle-hovered-container-color | — | |
--tanrei-comp-bottom-sheet-drag-handle-hovered-grip-color | — | |
--tanrei-comp-bottom-sheet-drag-handle-hovered-state-layer-color | — | |
--tanrei-comp-bottom-sheet-drag-handle-pressed-container-color | — | |
--tanrei-comp-bottom-sheet-drag-handle-pressed-grip-color | — | |
--tanrei-comp-bottom-sheet-drag-handle-pressed-state-layer-color | — | |
--tanrei-comp-bottom-sheet-drag-handle-state-layer-color | comp.bottom-sheet.drag-handle.grip.color | |
--tanrei-comp-bottom-sheet-focused-opacity | — | 1 |
--tanrei-comp-bottom-sheet-handle-container-height | comp.handle.container.breadth.medium | 24px |
--tanrei-comp-bottom-sheet-handle-container-shape | comp.handle.container.shape | 9999px |
--tanrei-comp-bottom-sheet-handle-container-width | comp.handle.container.length.medium | 48px |
--tanrei-comp-bottom-sheet-handle-grip-height | comp.handle.line.thickness.medium | 4px |
--tanrei-comp-bottom-sheet-handle-grip-width | comp.handle.line.length.medium | 32px |
--tanrei-comp-bottom-sheet-handle-margin-block | sys.space.space150 | 12px |
--tanrei-comp-bottom-sheet-header-gap | sys.space.space100 | 8px |
--tanrei-comp-bottom-sheet-header-height | — | 64px |
--tanrei-comp-bottom-sheet-header-padding-inline-end | sys.space.space150 | 12px |
--tanrei-comp-bottom-sheet-headline-color | sys.color.on-surface | |
--tanrei-comp-bottom-sheet-hovered-opacity | — | 1 |
--tanrei-comp-bottom-sheet-motion-enter-duration | sys.motion.role.appear.sheet.enter | 400ms |
--tanrei-comp-bottom-sheet-motion-enter-easing | sys.motion.easing.enter | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-bottom-sheet-motion-exit-duration | sys.motion.role.appear.sheet.exit | 350ms |
--tanrei-comp-bottom-sheet-motion-exit-easing | sys.motion.easing.exit | cubic-bezier(0.3, 0, 1, 1) |
--tanrei-comp-bottom-sheet-motion-feedback-duration | comp.handle.motion.feedback-duration | 450ms |
--tanrei-comp-bottom-sheet-motion-state-duration | comp.handle.motion.state-duration | 200ms |
--tanrei-comp-bottom-sheet-motion-state-easing | comp.handle.motion.state-easing | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-bottom-sheet-pressed-opacity | — | 1 |
--tanrei-comp-bottom-sheet-scrim-blur | comp.modal-layer.scrim.blur | 0px |
--tanrei-comp-bottom-sheet-scrim-color | comp.modal-layer.scrim.color | |
--tanrei-comp-bottom-sheet-headline-typescale | sys.typescale.heading-5 | 500 16px/1.5 |
フィクスチャ
基準となる描画パターンです。全プラットフォームが同じ ID でスナップショットを撮り、結果を比較できるようにしています。
modal
open: true handleLabel: Resize sheet label: Save photo 400 × 720 のウィンドウに単独で描画modal-partial
open: true detent: partial handleLabel: Resize sheet label: Terms of service 400 × 720 のウィンドウに単独で描画modal-full
open: true detent: full handleLabel: Resize sheet label: Terms of service 400 × 720 のウィンドウに単独で描画no-handle
open: true handle: false label: Save photo 400 × 720 のウィンドウに単独で描画standard
variant: standard open: true handleLabel: Resize sheet label: Nearby places 400 × 720 のウィンドウに単独で描画modal-header
open: true handleLabel: Resize sheet headline: Save photo closable: true closeLabel: Close 400 × 720 のウィンドウに単独で描画no-handle-header
open: true handle: false headline: Save photo closable: true closeLabel: Close 400 × 720 のウィンドウに単独で描画standard-header
variant: standard open: true handleLabel: Resize sheet label: Cafés and bakeries nearby headline: Nearby places closable: true closeLabel: Close nearby places 400 × 720 のウィンドウに単独で描画生成元 specs/bottom-sheet.spec.json, tokens/comp/bottom-sheet.tokens.json, design/tokens/
横に余裕のないコンパクトな画面で、その画面に属してはいるものの主題ではないものを見せるときに使います。地図上の地点の近くにある場所、写真の共有先、短い選択やフォームなどです。シートは、親指にいちばん近い下端からせり上がります。
幅の広いウィンドウで同じ種類のコンテンツを見せるなら、コンテンツに重ねずに横へ置けるサイドシートを使います。答えがないと作業を先に進められない判断にはダイアログを使います。プロダクトの行き先を並べるのはボトムシートではなく、ナビゲーションドロワーの役目です。
メインアクションから複数のアクションを出す。 画面のメインアクションが関連するいくつかの形(新しいドキュメント、フォルダー、アップロードなど)に分かれるときは、メインアクションボタンからそれらを並べたモーダルのボトムシートを開きます。Tanrei システムでは、提供しない FAB メニューの代わりにこの形を使います。
メインアクションから複数のアクションを出す:ボタンでシートを開きます
| 形 | どういうものか | 用途 |
|---|---|---|
| モーダル(既定) | モーダルなオーバーレイです。ウィンドウ全体を暗くするスクリムの上にせり上がります。外側はすべて操作不能になり、閉じるまでフォーカスは中にとどまります。 | 終えるか閉じるかしてから元の画面に戻る手順。共有、選択、短いフォームなど。 |
| スタンダード | レイアウト領域です。スクリムを描かずに親要素の下部に重なり、上のコンテンツはそのまま使えます。Tab でシートとコンテンツの間を行き来でき、フォーカスがシートから出ても閉じません。 | 画面と一緒に使うコンテンツ。地図の上に出す場所のリストなど。 |
2 つの形は同じトークンを読みます。違いはスクリムと振る舞いだけなので、デザインシステムが両者の見た目を変えることはありません。
レイアウト領域は、スタンダードのナビゲーションドロワー(折りたたんだ形を含みます)、アプリバー、スタンダードのサイドシートで、いずれもコンテンツの横か上に場所を取るものです。スタンダードのボトムシートは親要素の一部を覆う点で違いますが、これもレイアウト領域です。大事なのは形ではなく振る舞いだからです。モーダルではなく、スクリムを描かず、開いている間もコンテンツが使えます。フォーカスが出るたびに閉じてしまうシートでは、それが説明している地図と行き来しながらキーボードで使うことができません。
スタンダードのシートは、覆っているものを動かしたり余白を足したりしません。スクロールするコンテンツの最後の部分をシートの上に見せておきたいときは、そのコンテンツにシートの partial の高さ分の余白を付けてください。
detent には 2 つの値があります。partial では、覆う領域の一定の割合(既定のトークンでは半分)の高さになります。full では、領域の上端から少し余白を残したところまで伸びます。領域とは、モーダルのシートならウィンドウの上側のインセットより下の部分、スタンダードのシートなら親要素です。オンスクリーンキーボードが出ている間は領域がキーボードの上端で終わるので、シートはキーボードの上に載ります。
シートの高さはコンテンツの高さまでです。コンテンツが partial の高さに収まるシートは高さが 1 つしかなく、detent を変えても何も変わりません。コンテンツがそれより高いときはシートに 2 つの高さがあり、人はドラッグかドラッグハンドルで行き来できます。そのたびに新しい高さを添えて detentChange を発火します。Web、Compose、Flutter、React Native では、人が選んだ高さは、利用者側で detent を設定し直すまで保たれます。SwiftUI では detent のバインディングに書き込まれます。利用者側で detent を設定したときは、シートは動きますが何も発火しません。
ドラッグハンドルはシートの上端にある短いバーで、コンテンツがスクロールしても動かない行に置かれます。handle がオンなら、どのシートにも描かれます。
- 動かせる先があるときだけコントロールになる。 シートに 2 つの高さがある間、ハンドルは
handleLabelを名前とするボタンで、full の高さのときは展開状態になります。押すか Enter か Space で、シートはもう一方の高さへ動きます。高さが 1 つのときは、ドラッグできることを示す印として静止状態で描かれるだけで、フォーカスも押下もできず、支援技術からも見えません。 - ターゲット。 バー自体は小さくても、ハンドルを横切る行全体がヒット領域で、どの方向にも最小ターゲット以上あります。
- ドラッグ。 ハンドルの行、コンテンツのまわりの余白、コンテンツそのもののどこからでも、シートを上下にドラッグできます。指やポインターがプラットフォームのタッチスロップ(プラットフォームが定めていなければ 8 ポイント)を超えて動くとシートが追従し始め、full の高さより上には行きません。
- 離したとき。 フリングならその向きの次の高さに、ゆっくり離したなら近いほうの高さに落ち着きます。フリングとは、プラットフォームの最小フリング速度(定めがなければ 1 ミリ秒あたり 0.5 ポイント)より速く離すことです。
- ドラッグで閉じる。
dismissibleが true の間は、一番低い高さの半分より下で離すか、一番低い高さから下へフリングすると、理由dragで閉じるよう求めます。シートはドラッグで置かれた位置に 1 フレームとどまり、利用者側でopenを false にしていれば滑り出て、そうでなければ元の高さに戻ります。dismissibleが false の間は、一番低い高さに戻ります。 - スクロールするコンテンツ。 コンテンツを上へドラッグすると、まずシートが full の高さまで上がり、それからコンテンツがスクロールします。下へドラッグすると、まずコンテンツが先頭までスクロールし、それからシートが下がります。1 つのジェスチャーをシートからコンテンツのスクロールへ途中で渡せないプラットフォームでは、後半にもう 1 回ドラッグが要ります(下の既知の制約を参照)。キーボードや支援技術からは、どちらの高さでもコンテンツをスクロールできます。
ドラッグだけが唯一の手段になることはありません。ハンドルは 1 回押すかキーボードで高さを変えられ、モーダルのシートはスクリム、Escape、「戻る」でも閉じられます。スタンダードのシートと、dismissible が false のモーダルのシートには、ドラッグ以外に閉じる手段がないので、閉じるためのボタンを置いてください。
- 開く。 シートは下端の外からせり上がります。モーダルのシートは、同時にウィンドウ全体にスクリムをフェードインします。最初から
openが true の状態で表示したシートは、動きなしでその場に現れます。スタンダードのシートはフォーカスをそのままにします。モーダルのシートは、開いた直後と同じようにフォーカスを中へ移します。後ろの要素はもうフォーカスを受け取れないからです。 - フォーカスが入る。 どちらの形でも、開くと、指定した要素か、指定がなければシートそのものへフォーカスが移ります。フォーカスは、動きが終わってからではなく、開いた時点で移ります。
- フォーカスをとどめる(モーダルのみ)。 モーダルのシートでは、最後のフォーカスできる要素で Tab を押すと最初へ、最初の要素で Shift+Tab を押すと最後へ移ります。スクリーンリーダーで移動しても外へは出ず、背後のコンテンツもスクロールしません。スタンダードのシートは何もとどめません。Tab でコンテンツへ進み、シートは開いたままです。
- 閉じる。 フォーカスが中にある状態でシートが閉じると、開いたときにフォーカスを持っていた要素へフォーカスが戻ります。
- 重ねる。 モーダルのシートから開いたダイアログ、メニュー、別のモーダルのシートは、その上に重なります。Escape で閉じるのは一番上のものだけで、それが閉じると、そのオーバーレイが返すフォーカスをシートが受け取ります。
- 動き。 動きを減らす設定では、滑らせずにその場でフェードイン、フェードアウトします。すべてのアニメーションを取り除く Android の設定では、瞬時に現れて消えます。
- シートで何をするのかがすぐ分かるようにします。コンテンツの一番上にタイトルを置き、ほかに閉じる手段がないシートには「完了」や「閉じる」のボタンを置きます。
- 行が自分で余白を持つリストでは
insetをオフにして、行が端まで届くようにします。 - コンテンツが partial の高さを超えうるシートでは、キーボードやスイッチで操作する人も高さを変えられるよう、ハンドルをオンのままにします。
- シートを開いている間も上のコンテンツが必要なら、スタンダードの形を使います。
- シートの中のコントロールで作業を終えてもらう必要があるとき以外に、
dismissibleを false にしない。 - 人の手を止めなければならない判断にモーダルのシートを使わない。ダイアログを使います。
- プロダクトのナビゲーションをボトムシートに置かない。ナビゲーションドロワーかナビゲーションバーを使います。
- 手順のある作業のためにシートを何枚も重ねない。1 枚のシートの中身を切り替えます。
- ロール
dialog- ラベル付け
- The sheet is named by label, or by its headline where label is empty: a modal dialog while variant is modal, a region while it is standard. The headline is a heading; the close button is named by closeLabel; the divider is decorative. The drag handle, while the sheet has two heights, is a button named by handleLabel with the expanded state (true at full height); at one height it is hidden. The grip is decorative.
- 最小ターゲット
- 見た目のサイズが小さくても、ヒット領域は最小ターゲットサイズを満たします。 minimum 32px · pointer 44px · spacing 8px · touch 48px
キーボード操作
| キー | 動作 |
|---|---|
| Tab / Shift+Tab | Moves focus through the drag handle (while it is interactive), the close button (while closable is true) and the content's focusable elements, in that order. In an open modal sheet it wraps and never leaves the sheet; from a standard sheet it moves on to the content, and the sheet stays open. |
| Enter / Space | On the drag handle, moves the sheet to its other height (partial or full). On the close button, asks the sheet to close (close, reason close). |
| Escape | Asks an open modal sheet to close (close, reason escape) while dismissible is true; with stacked overlays, only the topmost. A standard sheet ignores it. |
備考
- Dragging is never the only way (WCAG 2.5.7): the drag handle changes the height with a single press or from the keyboard, and a modal sheet closes with Escape, back or a press on the scrim. A standard sheet, and a modal one whose dismissible is false, closes only by a drag, its close button or the product: set closable, or put a Close or Done button in its content.
- The standard form is a layout region, although it covers the bottom of the content rather than pushing it aside: the person works in the content and in the sheet at once, so focus leaving it never closes it. The modal form is a modal overlay.
- Web: the modal form is a native <dialog> opened with showModal() inside the shadow root, so the rest of the page is inert and the sheet is in the top layer; ::backdrop draws the scrim. Its cancel event (Escape) is cancelled and becomes close with the reason escape; the product then sets open to false. The standard form is a region (a <section> named by label) positioned at the bottom of its containing block. The `autofocus` attribute on a slotted element marks initial focus.
- Where a platform presents the modal sheet in a window of its own (Compose's Dialog, React Native's Modal), the window's own dim and transition are switched off and Tanrei draws the scrim and the motion, so every platform draws the same color.
- The sheet does not move or pad the content a standard sheet covers; a product whose content must stay visible above the sheet pads its scrolling content by the sheet's partial height.
- How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.
生成元 specs/bottom-sheet.spec.json, tokens/sys/measure.tokens.json
- モーダルのシートは、
label(空のときはheadline)を名前とするモーダルなダイアログとして伝えます。スタンダードのシートは、同じように名前の付いた領域(Web ではランドマーク)です。 - 見出しは見出しの要素です。閉じるボタンは
closeLabelを名前とする Tanrei のアイコンだけのボタンで、最小ターゲット、フォーカスリング、状態はボタン自身のものです。どちらの形でも、dismissibleにかかわらず、理由closeでシートに閉じるよう求めます。ヘッダーの下の区切り線は装飾です。 - モーダルのシートが開いている間は、外側のものをすべて操作不能にして支援技術から隠し、フォーカスとスクリーンリーダーの移動を中にとどめます。スタンダードのシートは何も操作不能にしません。
- 開くと、指定した要素かシートへフォーカスを移します。閉じると、開いたときにフォーカスを持っていた要素へ戻します。どちらの形でも同じです。
dismissibleが true の間は、Escape、プラットフォームの「戻る」、スクリムの押下でモーダルのシートに閉じるよう求めます。スタンダードのシートは Escape と「戻る」を無視します。- ドラッグハンドルはハンドルで、常に表示され、シート自身のトークンで描かれます。シートに 2 つの高さがある間は、
handleLabelを名前とするフォーカスできる 1 つのボタンで、full の高さでは展開状態です。Enter と Space でもう一方の高さへ動かします。高さが 1 つのときは支援技術から隠します。つまみの部分は常に隠します。 - ハンドルのヒット領域は最小ターゲット以上あり、フォーカスリングはキーボードフォーカスのときだけ表示します。
- コンテンツはシートの中でスクロールし、どちらの高さでもキーボードフォーカスや支援技術からスクロールできます。
- 文字を大きくすると、シートは detent が許す高さまでコンテンツとともに伸び、それを超える分はスクロールします。ハンドルは拡大しません。
- シートの色は下側のシステムインセットの下まで届き、コンテンツはそのインセットとオンスクリーンキーボードの上に保ちます。
labelもheadlineもないシート、handleLabelのない操作できるハンドル、closeLabelのない閉じるボタン、ハンドルも閉じるボタンもないのに閉じられるシート(どう閉じるのかが画面のどこにも示されない)、variant、detent、dividerVisibilityの知らない値は、デバッグビルドで報告します。
プラットフォームごとの既知の制約:
- マウスでコンテンツをドラッグすると、タッチと同じくシートが動きます。そのため、シートがまだ動ける間は、下へなぞってシートの中の文章を選択することはできません。
- SwiftUI、Compose、Flutter、React Native は、まだヘッダーを描きません。
headline、closable、closeLabel、dividerVisibilityと理由closeは、今のところ Web だけです。 - Web:タッチでは、ジェスチャーを誰が受け取るかをブラウザーが始まりの時点で決めるため、シートとコンテンツのスクロールの受け渡しにはもう 1 回のジェスチャーが要ります。マウスやペンでは 1 回のドラッグの中で受け渡します。タッチでのドラッグは、まだテストで確かめていません。Firefox では、ダイアログと同じく、シートは下へ滑らずにすぐ消えます。
- SwiftUI には、ダイアログのロールも領域のロールもありません。シートは
labelを名前とするコンテナで、モーダルの間はモーダルのトレイトを持ちます。ハンドルには展開の状態がないため、値として「expanded」か「collapsed」を英語でだけ伝えます。印がないときにシートそのものへフォーカスを移すことができず、閉じてもキーボードのフォーカスをどこにも戻しません。iOS では、モーダルのカバーが VoiceOver のフォーカスを戻します。macOS では、モーダルのシートはモディファイアーを付けたビューしか覆わないので、ウィンドウのルートに付けてください。 - iOS の SwiftUI:full の高さより下では、コンテンツをドラッグするとシートが上がり、スクロールは次のドラッグからになります。その高さではコンテンツはスクロールせず、VoiceOver やキーボードからもスクロールできません。コンテンツの素のテキストは本文のフォントになりますが行の高さは付かないため、2 行で 40 ポイントになり、ほかのプラットフォームの 44 より低くなります。
- SwiftUI:下端のインセットとオンスクリーンキーボード、ハードウェアキーボード、Escape、iOS の「戻る」ジェスチャーは、実機で試しておらず、押して確かめるテストもありません。
- Compose:シートはコンテンツを自分でスクロールさせるので、遅延リストではなく普通の列を渡してください。モーダルのシートが閉じるとき、そのウィンドウは退場の動きが終わるまでフォーカスを持ったままです。ドラッグは Robolectric でだけ確かめており、実機では試していません。
- Flutter:スタンダードのシートは名前付きのコンテナで、領域のランドマークではありません。Flutter 3.32 にそのロールがないためです。実機の TalkBack と VoiceOver では、まだ確かめていません。
- React Native は、どの要素がフォーカスを持っていたかを知ることができません。そのため、ダイアログやナビゲーションドロワーと同じく、シートが閉じるとフォーカスは開いた要素ではなく、プラットフォームが決める場所へ移ります。
initialFocusRefを渡さないときは、開いてもスクリーンリーダーだけがシートへ移ります。 - React Native は、1 つのジェスチャーをシートからコンテンツのスクロールへ途中で渡せないため、そのドラッグの残りは捨てられ、次のドラッグでスクロールします。Modal、ドラッグ、キーボードの回避、インセットは Jest でだけ確かめており、実機では試していません。
cupertinoでは、つまみを Apple の淡い輪郭の色で描くため、シートに対するコントラストはライトスキームで 1.8:1、ダークスキームで 2.2:1 となり、コントロールの境界に必要な 3:1 に届きません。コントラストを中または高にしたスキームでは 4.5:1 以上になります。3:1 に届かない 2 つの値は参考として残す計測値で、模倣プリセットには Tanrei のアクセシビリティ基準を適用しません。m3-likeでは、full の高さでの上端の余白は既定のシステムの値のままです。Material の値は、忠実度の参照データがボトムシートを扱うようになるまで保留しています。
- すべてのシートに「写真を保存」のような
labelかheadlineを付けます。ダイアログや領域の名前になります。 - ハンドルには、プロダクトの言語で「シートの高さを変更」のような
handleLabelを付けます。 - シートは利用者側で閉じます。
closeを受けたとき(Web ではcloseイベント、Compose、Flutter、React Native ではonClose)と、自分で置いた「完了」や「閉じる」のボタンから、openを false にします。SwiftUI ではシートがバインディングに false を書き込みます。 - スタンダードのシートと、
dismissibleが false のモーダルのシートには、目に見える閉じるボタンを置きます。シートの閉じるボタン(closableとcloseLabel)でも、利用者側のボタンでも構いません。ドラッグだけを出口にしてはいけません。 - 短いフォームの最初の入力欄など、役に立つときは最初のフォーカスを指定します。
- React Native では
initialFocusRefを渡し、シートが閉じた後に開いた要素へのフォーカスの戻しを利用者側で行い、ウィンドウのインセットをシートのinsetsかTanreiProviderに渡します。