メインコンテンツへスキップ
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
はじめにWebReactSwiftUIJetpack ComposeFlutterReact Nativeプリセット自分のデザインシステムを作るAI エージェントと Tanrei を使うブランドデザイントークンデザインシステムとカスタマイズTheme Studioカラータイポグラフィアイコン形状エレベーションモーションインタラクション状態振る舞いレイアウトアクセシビリティ言葉カラートークンタイプスケールトークン形状トークンエレベーショントークンモーショントークン状態トークンスペースと寸法のトークンコンポーネントボタンメインアクションボタンセグメントボタンボタングループメニュー付きボタンツールバーコピーボタンチェックボックスラジオボタンスイッチチップメニュースライダースライダーフィールドカレンダーDateTimePickerフィールドグループラジオグループチェックボックスグループセレクトセレクトフィールドメニューバーファイルアップロードファイルピッカー 上級者向けドロップゾーンテキスト入力欄 上級者向けテキストフィールドチップフィールドフィールドコンボボックス 上級者向けコンボボックスフィールド数値入力欄 上級者向け数値フィールドカラー入力欄 上級者向けカラーフィールドワンタイムコード入力欄 上級者向けワンタイムコードフィールド埋め込みテキスト入力欄 上級者向けチャットコンポーザーカード区切り線モーダルレイヤー 上級者向けダイアログリストリスト行ボトムシートサイドシートカルーセルコードブロックテーブルポップオーバーステップアコーディオン説明リスト図アプリシェルページヘッダーツリービュータイムライン画像プレビューハンドルサイズ変更できる枠スクロールエリアバッジプログレスインジケーターメーターナンバーティッカーローディングインジケータースナックバーツールチップキーボードショートカットタグインラインコードコールアウトアバター空の状態タググループスケルトンファイル項目色見本マーキーテキストタブタブビューアプリバーナビゲーションバーナビゲーションドロワー検索リンクページネーション前後のリンクステッパーパンくずリスト目次リンクカードFAB メニューステータスTanrei の作り方コントリビューションライセンスと帰属
ホームコンポーネントダイアログ

ダイアログ

作業を先に進めるのに欠かせない判断や入力のために、人をいったん立ち止まらせます。背後のすべてを暗くしてふさぎ、答えるか閉じるまで表示し続けます。

content_copyMarkdown で表示
カテゴリ
コンテインメント
トークン
comp.dialog
レベル
合成
構成要素
Modal layer

概要

Web
tanrei-dialog
schedule
SwiftUI
tanreiDialog
schedule
Jetpack Compose
TanreiDialog
schedule
Flutter
TanreiDialog
schedule
React Native
Dialog
schedule

生成元 docs/status/status.json, specs/dialog.spec.json

仕様は改訂済み、各プラットフォームは追従待ち

このページは、仕様の現在の形のダイアログを説明しています。どのダイアログも少なくとも 1 つのアクションを持ち、アクションは役割で配置される Tonal のボタンが横幅いっぱいに並びます。アイコンは先頭にそろえた見出しの横に付き、コンパクトなウィンドウではダイアログが下端に表示されます。Web コンポーネントはこの改訂に対応しており、このページのデモは、現在提供している Web コンポーネントです。上の表で Web が追従待ち(lagging)のままなのは、改訂の確認とベースラインの記録が済んでいないためです。SwiftUI、Compose、Flutter、React Native は、追従するまで以前の版を提供します。アイコンが中央そろえの見出しの上に付き、テキストのアクションが渡した順に末尾側にそろえて 1 行に並び、アクションは省略でき、どのウィンドウでも中央に表示される形です。

確認のダイアログ:ボタンで開きます

下書きを破棄キャンセル破棄

使い方

ダイアログは制御コンポーネントです。open を設定して開き、アクションのハンドラーの中か、dismiss を受けて open を false に戻して閉じます。アクションがダイアログを勝手に閉じることはないので、先に入力を確かめたり、進み具合を表示したり、エラーのときに開いたままにしたりできます。

ダイアログは、省略できるアイコン、見出し、省略できる補足テキスト、利用者側の任意のコンテンツ、そしてアクションでできています。ダイアログは対話なので、必ず 1 つ以上の答えを用意します。アクションは Tanrei のボタンで、それぞれに confirming(保存、削除、許可のように作業を完了する)か dismissive(キャンセル、後で、のように作業から引き返す)を付けます。ダイアログはこの印で配置を決めるので、渡す順序は問いません。variant を設定していないアクションは Tonal で描きます。

width を small、medium、large にすると、ダイアログはコンテンツの幅ではなく、決まった幅になります。ダイアログには閉じるボタンがありません。答えずに出ていく道は、キャンセルのような引き返すアクションです。

開いたときにフォーカスを受け取る要素は、プラットフォームの印で指定します。Web では autofocus、SwiftUI では .defaultFocus か @AccessibilityFocusState、Compose では initialFocus に渡す FocusRequester、Flutter では autofocus: true、React Native では initialFocusRef です。指定しなければフォーカスはダイアログそのものへ移り、見出しが最初に読み上げられます。

Web と React の例は現在の仕様に沿っています。SwiftUI、Compose、Flutter、React Native は追従待ちで、以前の版のダイアログをまだ提供しています。その版には confirming と dismissive の印も、Tonal の既定もないので、これらの例では各アクションに text のバリアントを指定し、完了するアクションを最後に置いています。追従するまで、例はその版のものです。

<tanrei-button id="discard-draft" variant="text">下書きを破棄</tanrei-button>

<!-- closedby="none" は dismissible を false にします。 -->
<tanrei-dialog headline="下書きを破棄しますか?"
               supporting-text="作成中のメッセージはこのデバイスから削除されます。">
  <!-- dismissive は先頭側、confirming は末尾側に置かれます。variant がなければどちらも Tonal です。 -->
  <!-- autofocus で最初のフォーカスを指定します:もっとも破壊的でないアクションです。 -->
  <tanrei-button slot="actions" dismissive autofocus>キャンセル</tanrei-button>
  <tanrei-button slot="actions" confirming>破棄</tanrei-button>
</tanrei-dialog>

<!-- 見出しの横のアイコンと、自分の variant を持つアクション。 -->
<tanrei-dialog headline="プロジェクトを削除しますか?" alert>
  <span slot="icon">delete</span>
  <tanrei-button slot="actions" dismissive variant="text">キャンセル</tanrei-button>
  <tanrei-button slot="actions" confirming variant="filled" destructive>削除</tanrei-button>
</tanrei-dialog>

<!-- フォーム:保存の横にキャンセル、そして決まった幅。ダイアログに閉じるボタンはありません。 -->
<tanrei-dialog headline="表示名を編集" width="small">
  <tanrei-text-input label="表示名" autofocus></tanrei-text-input>
  <tanrei-button slot="actions" dismissive>キャンセル</tanrei-button>
  <tanrei-button slot="actions" confirming>保存</tanrei-button>
</tanrei-dialog>

<script type="module">
  import '@tanrei/web/components/dialog';
  const dialog = document.querySelector('tanrei-dialog');
  const cancel = dialog.querySelector('[dismissive]');
  const discard = dialog.querySelector('[confirming]');
  document.querySelector('#discard-draft').addEventListener('click', () => (dialog.open = true));
  // アクションはダイアログを閉じません。ハンドラーの中で閉じます。
  cancel.addEventListener('click', () => (dialog.open = false));
  discard.addEventListener('click', () => { discardDraft(); dialog.open = false; });
  // Escape と背景の押下では、ネイティブの <dialog> と同じく cancel が発火します。
  // e.detail.reason がどれだったかを示します:escape、back、scrim。
  // その後ダイアログは自分で閉じます。イベントをキャンセルすれば閉じません。
  dialog.addEventListener('cancel', (e) => { if (unsaved()) e.preventDefault(); });
  // close は閉じるたびに発火します。
  dialog.addEventListener('close', onDialogClosed);
</script>
import { Button, Dialog } from '@tanrei/react';

// Escape と背景ではダイアログが自分で閉じます(onCancel、続いて onClose)。onClose で open を合わせます。
<Dialog
  open={open}
  headline="下書きを破棄しますか?"
  supportingText="作成中のメッセージはこのデバイスから削除されます。"
  onClose={() => setOpen(false)}
>
  <Button slot="actions" dismissive onClick={() => setOpen(false)}>キャンセル</Button>
  <Button slot="actions" confirming onClick={discard}>破棄</Button>
</Dialog>
import Tanrei

// .alert や .sheet と同じくモディファイアーです。閉じるよう求められると、バインディングに false が書き込まれます。
TanreiButton("下書きを破棄", variant: .text) { showDiscard = true }
    .tanreiDialog("下書きを破棄しますか?", isPresented: $showDiscard,
                  supportingText: "作成中のメッセージはこのデバイスから削除されます。",
                  actions: {
                      TanreiButton("キャンセル", variant: .text) { showDiscard = false }
                      TanreiButton("破棄", variant: .text) { discardDraft(); showDiscard = false }
                  })
import io.github.chnotchy.tanrei.components.TanreiButton
import io.github.chnotchy.tanrei.components.TanreiDialog
import io.github.chnotchy.tanrei.tokens.TanreiButtonVariant

// 開いている間だけ呼ぶのではなく visible で切り替えるのは、閉じるときの動きを最後まで見せるためです。
val cancel = remember { FocusRequester() }
TanreiDialog(
    visible = open,
    onDismissRequest = { open = false },
    headline = "下書きを破棄しますか?",
    supportingText = "作成中のメッセージはこのデバイスから削除されます。",
    initialFocus = cancel,
    actions = {
        TanreiButton(text = "キャンセル", onClick = { open = false }, modifier = Modifier.focusRequester(cancel), variant = TanreiButtonVariant.Text)
        TanreiButton(text = "破棄", onClick = { discardDraft(); open = false }, variant = TanreiButtonVariant.Text)
    },
)
import 'package:tanrei/tanrei.dart';

// showTanreiDialog はモーダルなルートをプッシュします。Navigator.pop で閉じ、閉じるよう求められたときは null で完了します。
final bool? discard = await showTanreiDialog<bool>(
  context: context,
  barrierDismissible: true, // 仕様の dismissible。Escape、戻る操作、スクリムで閉じられます
  builder: (BuildContext context) => TanreiDialog(
    headline: '下書きを破棄しますか?',
    supportingText: '作成中のメッセージはこのデバイスから削除されます。',
    actions: <Widget>[
      TanreiButton(
        variant: TanreiButtonVariant.text,
        autofocus: true,
        onPressed: () => Navigator.pop(context, false),
        child: const Text('キャンセル'),
      ),
      TanreiButton(
        variant: TanreiButtonVariant.text,
        onPressed: () => Navigator.pop(context, true),
        child: const Text('破棄'),
      ),
    ],
  ),
);
import { useRef, useState } from 'react';
import type { View } from 'react-native';
import { Button, Dialog } from '@tanrei/react-native';

const cancel = useRef<View>(null);
const [open, setOpen] = useState(false);

// React Native の Modal と同じ名前です。visible で制御し、onRequestClose が仕様の dismiss にあたります。
<Dialog
  visible={open}
  headline="下書きを破棄しますか?"
  supportingText="作成中のメッセージはこのデバイスから削除されます。"
  onRequestClose={() => setOpen(false)}
  initialFocusRef={cancel}
  actions={
    <>
      <Button ref={cancel} variant="text" onPress={() => setOpen(false)}>キャンセル</Button>
      <Button variant="text" onPress={discard}>破棄</Button>
    </>
  }
/>

仕様

アラートのダイアログ:開いた時点で読み上げる、破壊的な操作の確認

プロジェクトを削除deleteキャンセル削除

構成要素

パーツ 説明
1 scrim The modal layer's scrim over everything behind the dialog (MDL-1). It blocks presses and, when the dialog is dismissible, dismisses it when pressed.
2 container The raised surface that holds everything else: centred in the window, or, in a compact window, with the bottom sheet's appearance at its bottom edge (DLG-17). It is a dialog in both places: the bottom sheet's look, not its drag. Its width follows its widest part, or the size width names (DLG-27).
3 header The row at the top: the icon and the headline, and nothing else (DLG-26, DLG-28). By default the icon sits at the headline's start and both are aligned to the start; a system may put the icon above a centred headline instead (DLG-9).
4 icon · 任意 A glyph that marks the kind of decision (a warning, a deletion), at the headline's start.
5 headline The question or statement the dialog is about, aligned to the start. It is the dialog's accessible name.
6 scroll · 任意 The area between the header and the actions that holds the supporting text and the content, and scrolls them when the dialog does not fit (DLG-11). It reaches the container's inline edges whatever inline padding the container has, so the dividers cross the whole container and nothing overflows it; inside, it insets its parts by that same padding.
7 supporting-text · 任意 What will happen, or what the person needs to know to decide. It is the dialog's accessible description.
8 content · 任意 Whatever else the decision needs: a list to choose from, a field. The dialog insets it and hands it a content color; it does not lay it out.
9 divider · 任意 Hairlines between the header and the scrolling content and between the content and the actions, drawn only while the content scrolls: for as long as it does in the default system, or only while content lies beyond each line under dividerVisibility scroll.
10 actions The answers: one or more Tanrei Buttons, each marked confirming or dismissive, laid out as a full-width Button group (a row that 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. Required: a dialog asks something, so it always offers an answer.

プロパティ

プロパティ 型 既定値 説明
open boolean false Whether the dialog is presented. The product opens it, and closes it from an action's handler or in answer to dismiss.
headline string "" The question or statement ("Discard draft?"). Required: it is the accessible name.
supportingText string "" What will happen, or what the person needs to know. It is the accessible description.
alert boolean false Marks a dialog that interrupts with something urgent (a destructive confirmation, a lost connection): assistive technology reads its headline and supporting text as soon as it opens, and the web exposes it as an alertdialog.
dismissible boolean true Whether Escape, the platform's back and a press on the scrim ask to close the dialog (dismiss). Set it to false only when the person must choose one of the actions; give such a dialog a dismissive action. A dialog has no close button (DLG-28): content that needs a way out and nothing more gets a dismissive action (Cancel, or Close where there is nothing to cancel), or belongs in a side sheet or a bottom sheet.
width content | small | medium | large "content" What decides the container's width while the dialog is centred (DLG-7). content: as wide as its widest part, between comp.dialog.container.min-width and comp.dialog.container.max-width (DLG-7). small, medium, large: as wide as comp.dialog.width.small, comp.dialog.width.medium or comp.dialog.width.large (400, 560 and 800 in the default system) whatever it holds, for a form or content whose width should not follow its text (DLG-27). Never wider than the window less its margins; as a sheet in a compact window every value is placed as DLG-17 says. A structural choice drawn by code, not a token axis.
compactPresentation sheet | own "sheet" How the dialog is presented in a compact window, one narrower than comp.dialog.compact.window-width. sheet: with the bottom sheet's appearance at the window's bottom edge (DLG-17, DLG-18), as every dialog and every popover of Tanrei is there; it stays a dialog. own: centred in every window, as DLG-7 places it, for a dialog whose look must not change with the window (a short alert that should read as one). A structural choice drawn by code, not a token axis.
dividerVisibility auto | always | scroll "auto" When the dividers below the header and above the actions are drawn while the supporting text and the content scroll between them. always: both, for as long as that part scrolls, wherever it is scrolled to. scroll: each only while content lies beyond it — below the header while the part is not at its top, above the actions while more of it lies below. auto takes comp.dialog.divider.visibility, the design system's answer (always in the default system). Under every choice a dialog whose parts all fit draws none.

スロット

スロット 説明
default The custom content, below the supporting text.
icon A glyph at the headline's start (DLG-9).
actions The action items: Tanrei Buttons, one at least (DLG-19), each marked confirming or dismissive. The dialog places them (DLG-10) and gives those without a variant its own (DLG-20). A fixture fills it through its items.

イベント

イベント 説明
dismiss The person asked to close the dialog without an action: Escape, the platform's back or a press on the scrim. Carries which of these it was, named escape, back or scrim (in the platform's casing). Emitted only while dismissible is true.

アイテム

このコンポーネントが繰り返すアイテムの種類です。フィクスチャはアイテムを順に並べ、各プラットフォームはそれぞれの流儀で種類ごとに名前を付けます。

action

One answer at the foot of the dialog: a Tanrei Button, placed by the dialog as a full-width Button group places its items and given the dialog'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 dialog gives it the variant comp.dialog.actions.variant names, tonal in the default system (DLG-20). Set it to give one action another look (a filled Delete beside a text Cancel); a Delete that destroys something also sets the Button's own destructive, which the dialog leaves as set (BTN-40).
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 (OK, Save, Delete, Allow). 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, Not now, Keep editing). 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 dialog does not close by itself (DLG-6).
仕様 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 dialog reads, as the button group does. A Button without a variant attribute takes the dialog's variant through the internal controller the group already gives its items (DLG-20); 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 dialog's variant from an environment value the dialog 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 dialog'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 dialog's from an InheritedWidget (internal to the package). A Button in the actions prop (an element or a fragment of them). confirming and dismissive are props the dialog reads from its actions, as the button group reads them. A Button with no variant takes the dialog's from a React context (internal to the package).

状態

振る舞い

EARS 記法による要件です。各プラットフォームの実装は、この ID に対応するテストで検証されます。要件の原文は仕様ファイルのものをそのまま掲載しています。

ID 要件
DLG-1 WHEN open becomes true THE dialog SHALL present its container on its modal layer (MDL-1, MDL-2): above everything in its window, with the layer's scrim in comp.dialog.scrim.color and comp.dialog.scrim.blur over the whole window behind it, everything outside it inert and the content behind it kept from scrolling, until it closes.
DLG-2 WHEN the dialog opens THE dialog SHALL move focus as its modal layer does (MDL-3): 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 and the next Tab reaches the first focusable element.
DLG-3 WHILE the dialog is open THE dialog SHALL keep focus inside it as its modal layer does (MDL-4): Tab and Shift+Tab wrap between its last and first focusable elements, and screen-reader navigation does not leave it.
DLG-4 WHEN the dialog closes THE dialog SHALL return focus and restore the content behind it as its modal layer does (MDL-2, MDL-5).
DLG-5 WHILE dismissible is true, WHEN the user presses Escape, uses the platform's back or presses the scrim, as its modal layer answers them (MDL-6), THE dialog SHALL emit dismiss once with the reason escape, back or scrim; the product closes it by setting open to false, except on the web, where the dialog closes itself unless dismiss is cancelled, as a native dialog does. WHILE dismissible is false THE dialog SHALL ignore these inputs and emit nothing.
DLG-6 WHEN an action is activated THE dialog SHALL NOT close by itself; the product closes it from the action's handler by setting open to false.
DLG-7 WHILE the dialog is not presented as a sheet (DLG-17) — in a window at least comp.dialog.compact.window-width wide, or while compactPresentation is own — THE dialog SHALL centre its container in the window's safe area, at least comp.dialog.container.margin from every edge and from the on-screen keyboard, as wide as width names (DLG-27) — WHILE width is content, as wide as its widest part (its text, its content, or its actions in one row at their own widths) between comp.dialog.container.min-width and comp.dialog.container.max-width — but never wider than the window less the margins. There THE dialog SHALL draw its container in comp.dialog.container.color with comp.dialog.container.elevation and comp.dialog.container.shape corners 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).
DLG-8 THE dialog SHALL inset its parts by comp.dialog.container.padding on every side and stack the header (the icon and the headline, DLG-9), the supporting text and the content comp.dialog.container.gap apart, leaving out a part that is empty, with the actions comp.dialog.actions.margin-block-start below the last of them. It SHALL draw the headline in comp.dialog.headline.typescale and comp.dialog.headline.color, wrapping onto more lines rather than truncating, and the supporting text in comp.dialog.supporting-text.typescale and comp.dialog.supporting-text.color, and SHALL hand the content comp.dialog.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. THE dialog 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.
DLG-9 WHILE the icon slot is filled THE dialog SHALL draw the icon at comp.dialog.icon.size in comp.dialog.icon.color, with the family and fill of an unselected icon, sys.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max, and place it as comp.dialog.icon.placement says: WHILE it is inline, at the start of the header, comp.dialog.header.gap before the headline and centred on the headline's first line, so that a headline that wraps keeps the icon beside its first line; WHILE it is above, above the headline, comp.dialog.container.gap apart, with the icon and the headline centred. The supporting text and the content SHALL be aligned to the start in either placement, and WHILE the icon slot is empty the headline SHALL be aligned to the start whatever the placement.
DLG-10 THE dialog SHALL lay out its actions as a standard Button group lays out its items (BGR-12 to BGR-19), comp.dialog.actions.gap apart, 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 — with focus and reading order following that order (BGR-15). WHILE comp.dialog.actions.arrangement is fill it SHALL make them a full-width group across the container's inner width, the actions sharing a row equally (BGR-17) or each as wide as the column; WHILE it is end it SHALL keep them at their own widths, the row aligned to the end, and a column as wide as its widest action and aligned to the end. 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 (end), and otherwise in a column, re-deciding when the dialog's width or the text size changes.
DLG-11 WHEN the dialog's parts do not fit in the height its placement leaves THE dialog SHALL keep the header and the actions in place and scroll the supporting text and the content between them, and SHALL draw a divider of comp.dialog.divider.width in comp.dialog.divider.color across the container below the header and another above the actions: WHILE its divider visibility (DLG-24) is always, both for as long as that part scrolls, wherever it is scrolled to, at its top included; WHILE it is scroll, the one below the header only WHILE the scrolled part is not at its top and the one above the actions only WHILE more of it lies below. WHILE everything fits THE dialog SHALL draw no divider.
DLG-12 WHEN the header and the actions alone leave no room for the scrolled part (large text on a short window) THE dialog SHALL scroll all of its parts together instead, so that nothing is cut off or unreachable.
DLG-13 WHEN the dialog opens centred in the window (DLG-7) THE dialog SHALL fade its scrim in with its container, as its modal layer says (MDL-7), and make its container appear as every modal surface appears: from sys.motion.appear.opacity, sys.motion.appear.scale, sys.motion.appear.distance and sys.motion.appear.blur to whole, grown about its centre and travelling up into place, over comp.dialog.motion.enter-duration with comp.dialog.motion.enter-easing; WHEN it closes there it SHALL take the container back the same way to where it started while fading the scrim out, over comp.dialog.motion.exit-duration with comp.dialog.motion.exit-easing. A motion turned round SHALL go on from the opacity and the size it has reached. WHERE sys.behavior.appearance is unfold the scale SHALL apply on both axes, from the centre: a centred container has no anchored edge. WHERE motion is reduced it SHALL only fade the container in and out, over sys.motion.role.reduced, with no scale, distance or blur, or appear and disappear at once on a platform whose reduced-motion setting removes every animation (Android's). WHEN it is mounted with open true THE dialog SHALL appear without its motion while the document has not yet been first painted, and with it when it is mounted after that. Focus SHALL move (DLG-2, DLG-4) when the dialog opens or closes, not when the motion ends.
DLG-14 THE dialog SHALL expose its container as its modal layer's modal dialog (MDL-10) — an alert dialog WHILE alert is true — named by headline and described by supportingText; WHEN alert is true and the dialog opens it SHALL have assistive technology read the headline and the supporting text at once; WHILE headline is empty it SHALL still open, and in debug builds SHALL report that it has no accessible name.
DLG-15 WHEN a dialog opens while another is open THE dialog SHALL stack above it as modal layers stack (MDL-8): with its own scrim, the first one inert as well, focus returned on closing to the element inside the first dialog that had it, and Escape dismissing only the topmost dialog.
DLG-16 WHEN the layout direction is right-to-left THE dialog SHALL mirror its layout — the icon at the right of the headline, the headline and the supporting text aligned to the right, and its actions placed as a Button group places them right to left (BGR-8: dismissive at the right, confirming at the left of a row; a column's order unchanged) — and SHALL NOT mirror its content itself: the content follows the layout direction.
DLG-17 WHILE the window is narrower than comp.dialog.compact.window-width (a compact window) and compactPresentation is sheet THE dialog SHALL present its container with the bottom sheet's appearance (BSH-7): at the window's bottom edge, as wide as the window but no wider than comp.bottom-sheet.container.max-width and centred in it, whatever width names, filled with comp.bottom-sheet.container.color, raised by comp.bottom-sheet.container.elevation, its top corners rounded by comp.bottom-sheet.container.shape eased by sys.shape.corner-smoothing and its bottom corners square, its color reaching under the bottom system inset and its parts above it, sitting on the on-screen keyboard while it is shown, and never taller than the window below its top system inset less comp.bottom-sheet.container.margin-block-start. Its parts SHALL keep their own tokens and layout (DLG-8 to DLG-12, DLG-20), inside comp.dialog.container.padding. It SHALL stay a dialog there — DLG-1 to DLG-6, DLG-8 to DLG-12 and DLG-14 to DLG-16 hold unchanged, its scrim the dialog's own — and SHALL draw no drag handle, follow no drag and close on no swipe, because it has one height and its exits are its actions, Escape, back and the scrim. WHILE compactPresentation is own THE dialog SHALL be centred in every window as DLG-7 says. WHEN the window's width crosses comp.dialog.compact.window-width, or compactPresentation changes, while the dialog is open THE dialog SHALL take the other presentation at once, without motion, keeping focus where it is. WHERE comp.dialog.compact.window-width is 0 no window is compact. WHEN compactPresentation names a value the dialog does not define THE dialog SHALL treat it as sheet and in debug builds report the unknown value.
DLG-18 WHEN the dialog opens as a sheet in a compact window (DLG-17) THE dialog SHALL fade its scrim in (MDL-7) while sliding its container up from below the window's bottom edge into its place, as a modal bottom sheet opens (BSH-1), over comp.bottom-sheet.motion.enter-duration with comp.bottom-sheet.motion.enter-easing; WHEN it closes there it SHALL fade the scrim out while sliding the container down out of view, 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). Focus SHALL move as DLG-13 says.
DLG-19 IF the dialog opens with no action THE dialog SHALL still open and lay out its other parts, and in debug builds SHALL report as an error that a dialog must offer at least one action; a platform whose API can require an action (a required, non-empty list or builder of actions) SHALL require it, and one that can count its actions only when it opens SHALL count them then. It SHALL report likewise a dialog whose actions are all disabled while dismissible is false, which leaves the person no way out.
DLG-20 WHILE an action's variant is empty THE dialog SHALL give that Button the variant comp.dialog.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 dialog SHALL leave it as set. THE dialog SHALL NOT change an action's size or tokens, and SHALL NOT change its look or emphasis for its confirming or dismissive (BGR-19).
DLG-21 THE dialog 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 dialog is shown THE dialog SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state and its focus.
DLG-22 WHERE the layout direction is right-to-left THE dialog SHALL draw the glyph of its icon slot mirrored left to right exactly when the system's icon set marks that glyph's name as mirrored or the product forces mirroring for that use, and unmirrored when the product forbids it: the icon helper mirrors the glyph inside its own box, about the box's vertical centre line, changing neither its place, size, color and fill nor the order DLG-16 gives. THE dialog SHALL NOT mirror a glyph itself, and SHALL NOT mirror content the product gives as an image or a view of its own rather than by a glyph's name.
DLG-23 THE dialog SHALL lay out its headline and its supporting text 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 dialog SHALL still place each and align its lines by the layout direction, not by the text's own.
DLG-24 THE dialog SHALL draw its dividers (DLG-11) as its dividerVisibility names: WHILE dividerVisibility is auto, as comp.dialog.divider.visibility names, which follows sys.behavior.divider-visibility unless a design system sets the dialog apart; WHILE dividerVisibility is always or scroll, as that value names, whatever the tokens say. WHEN dividerVisibility, or the token it follows, changes THE dialog SHALL draw the other form at once, moving no part and keeping the scroll position and focus; WHEN dividerVisibility names a value the dialog does not define THE dialog 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.
DLG-26 THE dialog SHALL draw its header as one row that holds the icon and the headline as DLG-9 places them, across the container's inner width, and nothing at its end; WHILE headline is empty and the icon slot is empty THE dialog SHALL draw no header, leaving it out as DLG-8 leaves out an empty part.
DLG-27 WHILE the dialog is centred (DLG-7) and width is small, medium or large THE dialog SHALL make its container as wide as comp.dialog.width.small, comp.dialog.width.medium or comp.dialog.width.large, whatever its parts would ask and not held between comp.dialog.container.min-width and comp.dialog.container.max-width, but never wider than the window's safe area less comp.dialog.container.margin on each side; its headline and supporting text SHALL wrap to that width, its content SHALL take it, and its actions SHALL be placed in a row or a column as DLG-10 decides at that width. WHILE width is content THE dialog SHALL be as wide as DLG-7 says. WHILE it is a sheet THE dialog SHALL place its container as DLG-17 says, whatever width names, because a narrow window leaves no room for a choice of width. WHEN width changes while the dialog is open THE dialog SHALL take the new width at once, without a motion of its own, keeping its scroll position and focus; WHEN width names a value the dialog does not define THE dialog SHALL treat it as content and in debug builds report the unknown value.
DLG-28 THE dialog SHALL draw no close button in its header: a dialog asks for a decision, and its way out without one is its dismissive action and, WHILE dismissible is true, Escape, the platform's back and the scrim (DLG-5). THE dialog SHALL have no prop that draws one; WHERE a platform can be given a property the dialog does not define and the product gives one that asks for a close button (the web's closable and close-label attributes), THE dialog SHALL draw nothing for it and in debug builds SHALL report that a dialog has no close button.
DLG-29 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 dialog 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 dialog (DLG-14) — 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 dialog's name, over a name given so; WHILE headline is empty, a name given so is the dialog'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 dialog SHALL take it off that element.

プラットフォーム別の名前対応

パリティは API が同一であることを意味しません。同じ契約を、各プラットフォームの流儀で表現します。

仕様 Web SwiftUI Jetpack Compose Flutter React Native
コンポーネント tanrei-dialog tanreiDialog TanreiDialog TanreiDialog Dialog
open · プロパティ open isPresented visible showTanreiDialog visible
headline · プロパティ headline headline headline headline headline
supportingText · プロパティ supportingText supportingText supportingText supportingText supportingText
alert · プロパティ alert alert alert alert alert
dismissible · プロパティ closedby dismissible dismissible barrierDismissible dismissible
width · プロパティ width width width width width
compactPresentation · プロパティ compactPresentation compactPresentation compactPresentation compactPresentation compactPresentation
dividerVisibility · プロパティ dividerVisibility dividerVisibility dividerVisibility dividerVisibility dividerVisibility
dismiss · イベント cancel isPresented onDismissRequest showTanreiDialog onRequestClose
備考 supportingText is the supporting-text attribute. dismissible is HTML's own closedby attribute: any (the default: Escape and a press on the backdrop) or none. cancel is a CustomEvent whose detail.reason is escape, back or scrim, cancelable as on a native <dialog>; close follows every close. There is no closable or close-label attribute (DLG-28): given one, the dialog draws nothing for it, and debug builds report it. width is the width attribute (content, small, medium, large), and compactPresentation the compact-presentation attribute (sheet, own). Slots: default, icon, actions. The actions are laid out by the button group's placement (BGR-13 to BGR-17) inside the shadow root: the dialog's shadow root uses manual slot assignment, assigns its icon and content children to their slots, 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. Compact (DLG-17): a media query cannot read a custom property, so the dialog reads the resolved comp.dialog.compact.window-width through a probe and matches it with matchMedia('(width < …px)'), re-placing at once on change; the sheet's appearance is the shared one the popover draws too (src/primitives/compact-sheet.ts), read from comp.bottom-sheet. Debug builds (import.meta.env.DEV or the package's dev export) report a dialog opened with no assigned action (DLG-19). Snapshot harness: a 640 × 640 viewport, and 360 × 640 for the compact- fixtures, with the dialog opened by open; the screenshot is the viewport, since the dialog and its backdrop are in the top layer. dividerVisibility is the divider-visibility attribute (auto, always, scroll); auto reads --tanrei-comp-dialog-divider-visibility from the cascade with a style query, and where the browser has none the dialog writes the name it read on its <dialog> when it lays out. Lagging: ships the earlier revision — the icon above a centred headline, text actions in a row at the end, optional actions, centred in every window, the 480 × 640 harness. The target: a view modifier, as .alert and .sheet are: .tanreiDialog(_ headline:, isPresented: Binding<Bool>, supportingText:, alert:, dismissible:, icon:, actions:, content:). Dismissal writes false to the binding, SwiftUI's idiom. It is presented with .fullScreenCover, with a clear presentation background and the system transition off (Tanrei draws the scrim and the motion); that presentation makes the rest of the app inaccessible to VoiceOver, as accessibilityViewIsModal does. Initial focus is .defaultFocus or an @AccessibilityFocusState the product passes; the iOS escape gesture is .accessibilityAction(.escape). actions: is a required builder; the dialog counts its subviews through a variadic view and reports an empty one in debug builds (DLG-19), and lays them out with the button group's Layout. Compact is the window's width (a GeometryReader at the presentation's root) against the token, not the horizontal size class, so every platform switches at the same width. Snapshot harness: TanreiDialogSurface in place in a 640 × 640 frame, and 360 × 640 for the compact- fixtures. Not yet: the headline's letter-spacing by the script of its text (DLG-8, PRS-20). Not built on this platform yet: dividerVisibility (auto, always, scroll; DLG-24) — the dividers still follow the scroll position, as scroll does. Not built on this platform yet: width (DLG-27), and the reason dismiss carries. Not built on this platform yet: DLG-17 and DLG-18 as they are written now — the bottom sheet's appearance and motion in a compact window — and compactPresentation. Lagging: ships the earlier revision — the icon above a centred headline, text actions in a row at the end, optional actions, centred in every window. The target: TanreiDialog(visible, onDismissRequest, headline, supportingText, alert, dismissible, initialFocus: FocusRequester?, icon, actions, content) with actions a required parameter; the group's Layout sees its measurables and reports none in debug builds (DLG-19). visible rather than calling it only while open, so that the exit motion can run before the window goes. Presented in a Dialog window with usePlatformDefaultWidth = false and the window's own dim off (Tanrei draws the scrim). dismissible sets dismissOnBackPress and dismissOnClickOutside. The headline is the window's paneTitle. Compact is LocalWindowInfo.current.containerSize against the token, not a window size class, so every platform switches at the same width. Snapshot harness: renders TanreiDialogContent (the surface and the scrim) in place in a 640 × 640 dp box, and 360 × 640 for the compact- fixtures; the window itself is covered by instrumented tests. Not yet: the headline's letter-spacing by the script of its text (DLG-8, PRS-20). Not built on this platform yet: dividerVisibility (auto, always, scroll; DLG-24) — the dividers still follow the scroll position, as scroll does. Not built on this platform yet: width (DLG-27), and the reason dismiss carries. Not built on this platform yet: DLG-17 and DLG-18 as they are written now — the bottom sheet's appearance and motion in a compact window — and compactPresentation. Lagging: ships the earlier revision — the icon above a centred headline, text actions in a row at the end, optional actions, centred in every window, the 480 × 640 golden. The target: showTanreiDialog<T>(context:, barrierDismissible:, builder:) pushes a modal route that draws the scrim as its barrier, and returns a Future<T?>; Navigator.pop(context, value) from an action closes it, and a dismissal completes the future with null. TanreiDialog(headline:, supportingText:, alert:, icon:, required actions:, child:) is the surface the builder returns; actions is asserted non-empty (DLG-19). Initial focus is autofocus: true on a focusable widget. The route is marked scopesRoute and namesRoute, so assistive technology stays inside it. Compact is MediaQuery.sizeOf(context).width against the token. Snapshot harness: a golden of the whole 640 × 640 surface after showTanreiDialog, and 360 × 640 for the compact- fixtures, as the overlay is part of the widget tree. Not yet: the headline's letter-spacing by the script of its text (DLG-8, PRS-20). Not built on this platform yet: dividerVisibility (auto, always, scroll; DLG-24) — the dividers still follow the scroll position, as scroll does. Not built on this platform yet: width (DLG-27), and the reason dismiss carries. Not built on this platform yet: DLG-17 and DLG-18 as they are written now — the bottom sheet's appearance and motion in a compact window — and compactPresentation. Lagging: ships the earlier revision — the icon above a centred headline, text actions in a row at the end, optional actions, centred in every window, the 480 × 640 harness. The target: named as React Native's Modal: visible and onRequestClose (Android's back and, here, Escape on a hardware keyboard and a press on the scrim). Presented in a transparent Modal with animationType="none" (Tanrei draws the scrim and the motion), statusBarTranslucent and navigationBarTranslucent. Initial focus is an initialFocusRef the product passes, moved with AccessibilityInfo.setAccessibilityFocus. actions is required in the types, and development builds report it empty when the dialog becomes visible (DLG-19). Compact is useWindowDimensions().width against the token. Snapshot harness: renders DialogSurface (the surface and the scrim) in place in a 640 × 640 view, and 360 × 640 for the compact- fixtures, since a Modal is a native window the snapshot does not include. Not yet: the headline's letter-spacing by the script of its text (DLG-8, PRS-20). Not built on this platform yet: dividerVisibility (auto, always, scroll; DLG-24) — the dividers still follow the scroll position, as scroll does. Not built on this platform yet: width (DLG-27), and the reason dismiss carries. Not built on this platform yet: DLG-17 and DLG-18 as they are written now — the bottom sheet's appearance and motion in a compact window — and compactPresentation.

トークン

トークン 参照先 解決値(ライト)
--tanrei-comp-dialog-actions-arrangement — 0
--tanrei-comp-dialog-actions-gap sys.space.space200 16px
--tanrei-comp-dialog-actions-margin-block-start sys.space.space300 24px
--tanrei-comp-dialog-actions-variant — 1
--tanrei-comp-dialog-compact-window-width — 600px
--tanrei-comp-dialog-container-color sys.color.surface-bright #ffffff
--tanrei-comp-dialog-container-elevation sys.elevation.level3 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 1px 0px rgb(0 0 0 / 0.04), 0px 4px 8px -4px rgb(0 0 0 / 0.04), 0px 16px 24px -8px rgb(0 0 0 / 0.06)
--tanrei-comp-dialog-container-gap sys.space.space200 16px
--tanrei-comp-dialog-container-margin sys.space.space200 16px
--tanrei-comp-dialog-container-max-width — 560px
--tanrei-comp-dialog-container-min-width — 280px
--tanrei-comp-dialog-container-padding sys.space.space300 24px
--tanrei-comp-dialog-container-shape sys.shape.corner.large 12px
--tanrei-comp-dialog-content-color sys.color.on-surface #1b1b1b
--tanrei-comp-dialog-divider-color sys.color.outline-variant #000000 · 8%
--tanrei-comp-dialog-divider-visibility sys.behavior.divider-visibility 0
--tanrei-comp-dialog-divider-width sys.border.hairline 1px
--tanrei-comp-dialog-header-gap sys.space.space150 12px
--tanrei-comp-dialog-headline-color sys.color.on-surface #1b1b1b
--tanrei-comp-dialog-icon-color sys.color.secondary #525252
--tanrei-comp-dialog-icon-placement — 0
--tanrei-comp-dialog-icon-size sys.icon.medium 24px
--tanrei-comp-dialog-motion-enter-duration comp.modal-layer.motion.enter-duration 350ms
--tanrei-comp-dialog-motion-enter-easing comp.modal-layer.motion.enter-easing cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-dialog-motion-exit-duration comp.modal-layer.motion.exit-duration 300ms
--tanrei-comp-dialog-motion-exit-easing comp.modal-layer.motion.exit-easing cubic-bezier(0.3, 0, 1, 1)
--tanrei-comp-dialog-scrim-blur comp.modal-layer.scrim.blur 0px
--tanrei-comp-dialog-scrim-color comp.modal-layer.scrim.color #000000 · 40%
--tanrei-comp-dialog-supporting-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-dialog-width-large — 800px
--tanrei-comp-dialog-width-medium — 560px
--tanrei-comp-dialog-width-small — 400px
--tanrei-comp-dialog-headline-typescale sys.typescale.heading-5 500 16px/1.5
--tanrei-comp-dialog-supporting-text-typescale sys.typescale.body 400 14px/1.5714

フィクスチャ

基準となる描画パターンです。全プラットフォームが同じ ID でスナップショットを撮り、結果を比較できるようにしています。

生成元 specs/dialog.spec.json, tokens/comp/dialog.tokens.json, design/tokens/

ガイドライン

使う場面

答えがないと作業を先に進められないときに使います。元に戻せない操作の確認、権限の確認、ページを離れずに 2〜3 項目を入力してもらうときなどです。ダイアログは人を立ち止まらせるので、そうするだけの価値があるときにだけ使います。

元に戻せる操作に「よろしいですか?」と聞く代わりに、スナックバーで結果を伝えて「元に戻す」を添えます。画面をふさがない短い選択にはメニューを使います。長いフォームや複雑な作業は、専用のページに置きます。

構成要素

要素 設定 補足
アイコン icon スロット 省略できます。delete のような、判断の種類を示すグリフです。見出しの 1 行目の先頭に 12 あけて置き、見出しが折り返しても 1 行目の横にとどまります。
見出し headline 必須です。「下書きを破棄しますか?」のような問いかけか文です。常に先頭にそろえます。ダイアログのアクセシブルネームになります。
補足テキスト supportingText 何が起きるか、判断に必要なことを書きます。常に先頭にそろえます。ダイアログの説明になります。
コンテンツ 既定のスロット 判断に必要なそのほかのもの。選択肢のリストや入力欄などです。ダイアログは余白を付け、コンテンツの色を渡しますが、中のレイアウトはしません。
アクション actions スロット 必須です。Tanrei のボタンを 1 つ以上置き、それぞれに confirming か dismissive を付けます。ダイアログは横幅いっぱいのボタングループとして配置し、variant のないものを Tonal で描きます。

要素がウィンドウに収まらないときは、ヘッダーとアクションはそのままで、その間の補足テキストとコンテンツがスクロールします。スクロールしている間は、先頭にあるときも含めて、ヘッダーとの間とアクションとの間に細い区切り線を引きます。dividerVisibility を scroll(Web では divider-visibility)にすると、それぞれの区切り線は、その向こうにスクロールで隠れている部分がある間だけ引かれます。cupertino と m3-like は、すべてのダイアログでこちらを選んでいます。文字が大きくてそれすらできないときは、ダイアログ全体がスクロールし、区切り線は引きません。

Web では、スクロールする部分は ::part(scroll) です。::part(container) の左右のパディングをどう変えても、この部分はコンテナの両端まで届き、内側に同じパディングを保ちます。そのため区切り線はダイアログの端から端まで引かれ、はみ出すこともありません。コンテンツの下端に position: sticky で行を固定しているなら、その行の高さをスクロールする部分に伝えてください。フォーカスが移ってスクロールで表示された入力欄が、その行の下に隠れずに済みます。

tanrei-dialog::part(scroll) { scroll-padding-block-end: 56px; }

補足テキストのない見出しだけのダイアログも使えます。見出しで問いかけ、その答えを並べれば対話になるからです。使えないのは、答えるもののない告知だけのダイアログです。

アクション

アクションはコンテナの内側の幅を均等に分け合い、間を 16 あけます。

  • 役割で配置する。 1 行のときは、引き返すアクションが先頭側、完了するアクションが末尾側です。縦に積むときは、完了するアクションが一番上、引き返すアクションが一番下です。どちらの印もないアクションは、両者の間で渡した位置を保ちます。フォーカスとスクリーンリーダーは配置した順にたどるので、ボタンを手で並べ替える必要はありません。
  • 収まるかどうかで 1 行か縦積みかを決める。 どのラベルも自分の幅に 1 行で収まる間は 1 行に並べます。1 つでも折り返しそうになると縦に積み、それぞれをダイアログの幅いっぱいにします。ダイアログの幅や文字の大きさが変わると、決め直します。
  • 既定は Tonal。 variant を設定していないアクションは Tonal で描きます。印が決めるのは位置だけで、見た目は変えません。
  • 別の見た目にする。 テキストのキャンセルの横に Filled の削除を置くように、1 つのアクションだけ見た目を変えたいときは、そのボタンの variant を設定します。削除や破棄を行うアクション(この削除など)には destructive も設定します。variant はそのままで、エラーの色になります。設定した variant も destructive も、ダイアログが上書きすることはありません。すべてのダイアログで見た目を変えたいプロダクトは、ボタンごとにではなくレイヤで一度だけ設定します(actions.variant、自分の幅のまま末尾側にそろえるなら actions.arrangement)。
  • 少なくとも 1 つ。 アクションのないダイアログも開きますが、デバッグビルドでエラーとして報告します。アクションを必須にできる API では必須にすると、仕様は定めています。SwiftUI の actions: ビルダー、Compose の actions 引数、Flutter の actions リスト、React Native の actions プロパティです。この 4 つのうち、いま必須なのは SwiftUI だけで、ほかは追従するまで、アクションのないダイアログも受け付けます。dismissible が false で、すべてのアクションが無効のダイアログも、出口がなくなるのでデバッグビルドで報告します。

閉じるボタンはない

保存の横にキャンセルがあるフォーム。small の幅

表示名を編集キャンセル保存

ダイアログは、見出しの行に閉じるボタンを描きません。ダイアログは判断を求めるものです。判断せずに出ていく道は引き返すアクションと、dismissible が true の間は、Escape、プラットフォームの「戻る」、スクリムの押下です。

  • フォームや選択。 完了するアクションの横に、キャンセルのような引き返すアクションを置きます。上の例のとおりです。
  • 読むもの。 「閉じる」というラベルのアクションを 1 つ置き(取り消すものがあれば「キャンセル」)、dismissive を付けます。答えは要らず出口だけが要る内容なら、ダイアログではなく、閉じるボタンのあるサイドシートか、スワイプで閉じられるボトムシートに表示します。
  • 長いダイアログ。 コンテンツがスクロールする間も、アクションは下端にとどまるので、出口は見えたままです。
  • Web では。 closable や close-label を付けても、ダイアログは何も描きません。デバッグビルドで、ダイアログに閉じるボタンはないことを報告します。

幅

既定(width が content)では、ダイアログはもっとも幅の広い部分に合わせ、280 から 560 の間の幅になります。幅がテキストに合わせて決まるので、確認に向いています。決まった幅にしたいときは width を設定します。フォームの入力欄やメッセージが変わっても、幅が変わりません。

width 幅 用途
content もっとも幅の広い部分、280〜560 確認や短い問いかけ(既定)
small 400 1〜2 個の入力欄
medium 560 フォーム、選択肢のリスト
large 800 表やプレビューのように、場所が要るコンテンツ

決まった幅も、ウィンドウの幅から左右 16 ずつを除いた幅を超えません。たとえば 640 のウィンドウでは、large のダイアログは 608 になります。コンパクトなウィンドウ(600 より狭い)では、どの幅も既定と同じく、下端のシートとして、最大 640 までウィンドウの幅いっぱいに置きます。開いている間に width を変えると、すぐにその幅で描きます。プロダクトはレイヤで、すべてのダイアログの 3 つの幅を変えられます(width.small、width.medium、width.large)。

コンパクトなウィンドウでは

ウィンドウの幅が 600 より狭いときは、ダイアログは中央に立つ代わりに、ボトムシートの見た目になります。これは、コンパクトなウィンドウでのすべてのダイアログとポップオーバーに対する Tanrei の規則です。ダイアログはウィンドウの下端(オンスクリーンキーボードが出ていればその上)に付き、comp.bottom-sheet.container.max-width(640)までウィンドウの幅いっぱいに広がり、ボトムシートの色とエレベーションで、上の角はボトムシートの角丸、下の角は直角で描かれます。色は下端のシステムの余白の下まで届き、中のパーツはダイアログ自身の余白と文字のスタイルを保ちます。見るのは幅だけなので、横向きのスマートフォンはコンパクトではなく、ダイアログは中央に表示されます。

見た目はボトムシートでも、ダイアログのままです。高さは 1 つだけで、出口はアクション、Escape、戻る、スクリムなので、ドラッグハンドルはなく、ドラッグに追従せず、スワイプでも閉じません。ロール、フォーカス、背後の操作不能、スクリム、閉じ方は、ほかのダイアログと同じです。開いている間にウィンドウの幅が 600 をまたぐと、動きを付けずにすぐもう一方の配置へ移り、フォーカスはそのままです。

どこでもダイアログを中央に表示するシステムは、コンパクトの幅を 0 にします。m3-like と cupertino のプリセットはそうしています。どちらの元のデザインも、ダイアログを中央に表示するからです。これらのプリセットは、元のデザインどおりのテキストのアクションも保ちます。1 つのダイアログだけをどのウィンドウでも中央に置くには、その compactPresentation を own(Web では compact-presentation="own")にします。

開く、フォーカス、閉じる

ダイアログはモーダルなオーバーレイです。フォーカスを中へ移し、中にとどめ、閉じたら返します。

  • 開く。 open が true になると、ダイアログはウィンドウのすべてより上に中央そろえで(コンパクトなウィンドウでは下端に)現れ、ウィンドウ全体をスクリムで暗くします。外側のものはすべて操作不能になり、フォーカスも押下もできず、支援技術からも届かなくなります。背後のコンテンツはスクロールしなくなります。
  • フォーカスが入る。 フォーカスは指定した要素へ、指定がなければダイアログそのものへ移ります。そのため支援技術は見出しを最初に読み、次の Tab で最初のフォーカスできる要素に届きます。フォーカスは、動きが終わってからではなく、開いた時点で移ります。
  • フォーカスをとどめる。 最後のフォーカスできる要素で Tab を押すと最初へ、最初の要素で Shift+Tab を押すと最後へ移ります。スクリーンリーダーで移動しても、ダイアログの外へは出ません。
  • 閉じるよう求める。 dismissible が true の間は、Escape、プラットフォームの「戻る」(Android の戻るボタンやジェスチャー、iOS のアクセシビリティの「戻る」ジェスチャー)、スクリムの押下で dismiss を 1 回発火し、利用者側で閉じます。Web では、ネイティブの <dialog> と同じく、cancel をキャンセルしない限りダイアログが自分で閉じます。dismissible が false の間はこれらの操作では何も起きず、アクションでしか閉じません。コンパクトなウィンドウの下端にあるダイアログも、スワイプでは閉じません。dismiss は、どれだったかを理由 escape、back、scrim で伝えます。
  • フォーカスが戻る。 閉じると、開いたときにフォーカスを持っていた要素へフォーカスを戻し、背後のコンテンツを元の状態に戻します。
  • 重ねる。 ダイアログから開いたダイアログは、自分のスクリムとともに上に重なり、最初のダイアログも操作不能にします。Escape で閉じるのは一番上のものだけで、閉じると、最初のダイアログの中でフォーカスを持っていた要素へ戻ります。
  • 読み上げ。 ダイアログとして、見出しを名前、補足テキストを説明として伝えます。alert を付けるとアラートダイアログになり、開いた時点で支援技術が見出しと補足テキストを読み上げます。
  • 動き。 スクリムがフェードインします。中央のコンテナは、少し小さい大きさからフェードインしながら広がります。コンパクトなウィンドウでは、コンテナはボトムシートのモーションで、広がらずに下端の外からせり上がり、閉じるときは下へ戻ります。動きを減らす設定では広がったり動いたりせずにフェードし、すべてのアニメーションを取り除く Android の設定では瞬時に現れます。

各プラットフォームでの表示方法は、Web では showModal() で開くネイティブの <dialog>、SwiftUI では iOS で背景を透明にした .fullScreenCover(全画面のカバーがない macOS では、モディファイアーを付けたビューの上のオーバーレイ)、Compose では Dialog のウィンドウ、Flutter ではモーダルなルート、React Native では透明な Modal です。どれもプラットフォーム自身の暗幕と遷移を切り、スクリムと動きはトークンから描くので、どのプラットフォームでも同じ見た目になります。

まだ表示されていないスナックバーは、Tanrei のダイアログが開いている間は待ち、表示中のものは時間切れを止めます。ダイアログの中のツールチップは、ダイアログより上に描かれます。

推奨

  • 見出しには問いかけそのもの、または文そのものを書きます。「下書きを破棄しますか?」「プロジェクトを削除しますか?」。「よろしいですか?」は避けます。
  • アクションには、何をするかが分かる名前を付けます。「破棄」「削除」「サインアウト」。「はい」「いいえ」は避けます。
  • どのアクションにも confirming か dismissive を付け、配置はダイアログに任せます。
  • どのダイアログにも答えを用意します。知らせるだけなら、スナックバーかページ上の 1 行にします。
  • 読むものを見せるダイアログには、「閉じる」か「キャンセル」というラベルのアクションを置きます。または、その内容をサイドシートに表示します。
  • 破壊的な操作の確認ではキャンセルに、フォームでは最初の入力欄に、最初のフォーカスを指定します。テキストを先に読んでほしいダイアログでは何も指定しません。
  • alert は、破壊的な操作の確認や接続の切断など、緊急の割り込みにだけ付けます。

非推奨

  • 作業がどうしても先に進めないとき以外に、求められていないダイアログを開かない。
  • 見出しやコンテンツに、自前の閉じるボタンを足さない。ダイアログに閉じるボタンはなく、出口は引き返すアクションです。
  • 人がアクションのどれかを必ず選ばなければならないとき以外に、dismissible を false にしない。スイッチやスクリーンリーダーの人は Escape や「戻る」に頼っています。閉じられないダイアログにも、キャンセルのような目に見える出口が必要です。
  • ダイアログに多くの項目を入れない。長いフォームはページに置きます。
  • 確定のアクションを最後にするためにアクションを手で並べ替えたり、Tonal にするためにすべてのボタンに variant を設定したりしない。どちらもダイアログが行います。
  • ボタンの見た目を variant 以上に変えたり、ダイアログの枠にコントロールを追加したりしない。
  • コンパクトなウィンドウの下端のダイアログを、ボトムシートとして扱わない。スワイプで閉じられるようにしたいなら、ボトムシートを使います。

アクセシビリティ

ロール
dialog
ラベル付け
The headline is the accessible name and the supporting text the description. The role is dialog, or alertdialog while alert is true (platforms without that distinction read the headline and supporting text on opening). Everything outside the dialog is hidden from assistive technology while it is open. 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).
最小ターゲット
見た目のサイズが小さくても、ヒット領域は最小ターゲットサイズを満たします。 minimum 32px · pointer 44px · spacing 8px · touch 48px

キーボード操作

キー 動作
Tab / Shift+Tab Move focus through the dialog's focusable elements, wrapping from the last to the first and back; focus never leaves the dialog.
Escape Dismisses the dialog (emits dismiss with the reason escape) while dismissible is true; with nested dialogs, only the topmost.
Enter / Space Activate the focused button (an action: its click). The dialog adds no default action to Enter.

備考

  • The dialog has no target of its own; its actions are Tanrei Buttons, which meet the minimum target themselves (BTN-5). comp.dialog.actions.gap keeps each action's whole target in a column in the default system.
  • Initial focus: mark the least destructive action (Cancel) in a destructive confirmation, the first field in a form, and nothing in a dialog whose text must be read first. Without a mark, focus goes to the container (DLG-2).
  • Every dialog offers an action (DLG-19). Use dismissible false sparingly: a person using a switch or a screen reader relies on Escape and back. A dialog that must not be dismissed still needs a visible way out, a dismissive action.
  • A dialog has no close button (DLG-28). Its visible way out is a dismissive action, which a person meets in the reading order of the actions; Escape, back and the scrim dismiss it as well while it is dismissible.
  • 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, so a sighted and a screen-reader user meet the actions in the same order.
  • In a compact window the dialog takes the bottom sheet's appearance (DLG-17), as every dialog and every popover of Tanrei does, and is still a modal dialog: the same role, focus, inertness and dismissal, and no drag. A person who expects a bottom sheet's swipe to close it uses Escape, back or the scrim, or a dismissive action. compactPresentation own keeps one dialog centred in every window.
  • Text size: at 200% the dialog grows up to the window; its actions stack when a label no longer fits its share (DLG-10), its supporting text and content scroll (DLG-11), and on a very short window everything scrolls together (DLG-12).
  • Web: a native `<dialog>` opened with `showModal()` inside the shadow root, so the rest of the page is inert and the dialog is in the top layer. `::backdrop` draws the scrim. The `autofocus` attribute on a slotted element marks initial focus. `form method="dialog"` closes it natively, and the native `close` event follows every close.
  • Where a platform presents the dialog in a window of its own (Compose's `Dialog`, React Native's `Modal`), the platform's own window dim is switched off and Tanrei draws the scrim, so every platform draws the same color.

生成元 specs/dialog.spec.json, tokens/sys/measure.tokens.json

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

  • ダイアログをモーダルなダイアログとして伝えます。alert が true の間はアラートダイアログです。見出しを名前、補足テキストを説明とし、alert のときは開いた時点で見出しと補足テキストを読み上げます。
  • 開いている間、外側のものはすべて操作不能にし、支援技術からも隠します。
  • 開くと、指定した要素かダイアログそのものへフォーカスを移します。Tab と Shift+Tab はダイアログの中で折り返します。閉じると、開いたときにフォーカスを持っていた要素へ戻します。
  • dismissible が true の間は、Escape、プラットフォームの「戻る」、スクリムの押下で dismiss を発火します。ダイアログが重なっているときは、一番上のものだけです。
  • Enter と Space はフォーカス中のボタンを実行します。ダイアログが Enter に既定のアクションを割り当てることはありません。
  • アクションは Tanrei のボタンなので、それぞれが最小ターゲットを満たします。縦に積んだアクションの間の 16 で、どのターゲットも欠けません。
  • フォーカスとスクリーンリーダーは、目で見るのと同じ、配置した順にアクションをたどります。
  • コンパクトなウィンドウの下端でも、ロール、フォーカス、背後の操作不能、閉じ方は変わりません。ボトムシートのようにスワイプで閉じると思った人は、Escape、「戻る」、スクリム、引き返すアクションで閉じられます。
  • 文字を 200% にすると、ダイアログはウィンドウいっぱいまで広がり、ラベルが自分の幅に収まらなくなったアクションは縦に積み、補足テキストとコンテンツがスクロールします。とても低いウィンドウでは全体がまとめてスクロールするので、切れたり届かなかったりする部分はありません。
  • 見出しやアクションのないダイアログも開きますが、名前や答えがないことをデバッグビルドで報告します。dismissible が false で、すべてのアクションが無効のダイアログも報告します。

プラットフォームごとの既知の制約:

  • SwiftUI、Compose、Flutter、React Native:dividerVisibility は、これらのプラットフォームの追従を待っています。これらの区切り線は scroll と同じく、その向こうにスクロールで隠れている部分がある間だけ現れます。
  • SwiftUI、Compose、Flutter、React Native:width と dismiss が伴う理由は、これらのプラットフォームの追従を待っています。
  • Web:Firefox では、閉じていくダイアログをフェードの間トップレイヤーに残せないので、閉じるときはフェードせずにすぐ消えます。
  • Web:オンスクリーンキーボードを避けることは、キーボードのある実機ではまだ確かめていません。
  • SwiftUI には、ダイアログとアラートダイアログのロールがありません。コンテナはモーダルのトレイトを持ち、見出しを名前、補足テキストをヒントとして説明します。見出しには、ドキュメントの見出し(heading)であることを伝えるトレイトが付きます。alert のときは、開くと同時に VoiceOver が見出しと補足テキストを読み上げます。
  • macOS の SwiftUI には全画面のカバーがないので、ダイアログが覆うのはモディファイアーを付けたビューだけです。ウィンドウのルートに付けてください。開いている間そのビューは無効になり、そのコントロールはスクリムの下で無効の状態を描きます。
  • SwiftUI は、キーボードフォーカスをダイアログそのものへ移せません。目印がなければ、VoiceOver は見出しから始まります。閉じるとき、iOS は VoiceOver のフォーカスを開いた要素へ戻しますが、キーボードフォーカスは iOS でも macOS でも戻りません。
  • Compose には、アラートダイアログのロールがありません。ウィンドウは見出しを名前とするダイアログで、alert のダイアログは開くときに見出しと補足テキストを読み上げますが、その Android の API は Android 16 で非推奨になりました。
  • Compose:実際のダイアログのウィンドウ、Tab の折り返し、開いた要素へのフォーカスの戻り、戻るジェスチャー、TalkBack は、Robolectric でしか確かめておらず、実機ではまだ確かめていません。
  • Flutter:見出しとアクションだけで場所がなくなり、ダイアログ全体をまとめてスクロールするときは、その切り替えをレイアウトの後に決めるので、1 フレーム遅れて表示されます。Flutter には説明のプロパティがないので、補足テキストはコンテナのヒントになります。スクリムには、barrierLabel を渡さない限りアクセシブルネームがありません。
  • React Native は、どの要素がフォーカスを持っていたかを知ることができません。そのため閉じても、ダイアログを開いた要素へはフォーカスを戻せず、iOS と Android が閉じたウィンドウに対して決める場所へ移ります。initialFocusRef を渡さないときは、スクリーンリーダーだけが見出しへ移り、ハードウェアキーボードのフォーカスは新しいウィンドウでプラットフォームが決めた場所のままです。
  • React Native:Tab がダイアログの中で折り返すかどうかはプラットフォーム次第です。それに加え、スクリーンリーダーのフォーカス、「戻る」、iOS の「戻る」ジェスチャー、重ねたダイアログは、実機ではまだ確かめていません。
  • React Native:ダイアログがセーフエリアの内側に収まるのは、TanreiProvider にウィンドウのインセットを渡したときだけです。

利用者側で行うこと

  • どのダイアログにも見出しを付けます。見出しがダイアログの名前です。
  • どのダイアログにもアクションを 1 つ以上置き、それぞれに confirming か dismissive を付けます。以前の版をまだ提供しているプラットフォームでは、完了するアクションを利用者側で最後に置きます。
  • ダイアログは利用者側で閉じます。各アクションのハンドラーの中と、dismiss を受けたとき(プラットフォームに応じて onRequestClose、onDismissRequest、バインディング、ルート)です。
  • 役に立つときは最初のフォーカスを指定します。破壊的な操作の確認ではキャンセル、フォームでは最初の入力欄です。
  • dismissible が false でも、キャンセルのような引き返すアクションを、目に見える出口として必ず用意します。
  • React Native では initialFocusRef を渡します。閉じた後、プラットフォームがフォーカスを開いた要素へ戻さない場合は、利用者側で戻します。
  • macOS の SwiftUI では、スクリムがウィンドウ全体を覆うように、.tanreiDialog をウィンドウのルートのビューに付けます。
  • Web でコンテンツの下端に行を固定しているときは、::part(scroll) の scroll-padding-block-end をその行の高さにします。フォーカスのある要素がその行の下に隠れなくなります。