sashigane
コンポーネント

Modal

モーダルダイアログ。画面を覆って、応答するまで先へ進ませない

素の dialog に乗っています

showModal() を呼ぶと、ブラウザが次の全部を持ってくれます。

  • 最前面の層に出る(z-index も overflow も関係しない)
  • フォーカスが中に閉じ込められる
  • 後ろが操作できなくなる
  • Escape で閉じる
  • 閉じたとき、開く前にフォーカスがあった場所へ戻る

自前で書くと、この5つを全部持ち直すことになります。

属性だけで出すと、この層に入りません。 入らないとフォーカスの閉じ込めも後ろの遮断も 効きませんが、見た目は出るので気づけません。テストが :modal で測っています。

開いているかどうかは利用側が持ちます

const modal = useModal();

<Button onClick={modal.show}>消す</Button>
<Modal open={modal.open} onClose={modal.hide} title="確認">
  本当に消しますか
</Modal>

これは利用側が知りたい値です——開いている間だけ読み込む、 閉じたら次の案内を出す、など。コンポーネントの中に隠すと「開いた」ことしか分かりません。

Escape で閉じたときも onClose が呼ばれます。返さないと、 利用側は開いていると思ったままになり、次に押しても開きません。

後ろの巻き取りは自分で止めています

showModal() は後ろの操作は止めますが、巻き取り(スクロール)は止めません。 止めないと、中を読んでいるつもりで後ろが動きます。

背景を押して閉じるのは既定ではありません

dialog は背景の部分も自分の領域なので、押した場所が中か外かを座標で見分ける ことになります。取り違えると、中を押したのに閉じます。 消える操作の前では代償が大きいので、closeOnBackdrop を渡したときだけにしています。

表示と消去にアニメーションが付きます

ダイアログと背景が一緒に薄れて現れ、薄れて消えます。

閉じるときが難しいところです。 閉じる操作は表示を即座に消すので、 何もしないと消えてから動くことになり、動きが見えません。 display と overlay を allow-discrete で遷移させて、 動き終わるまで最前面の層に留まるようにしています。

動きを減らす設定(prefers-reduced-motion)では動きません。 尊重するのは利用側の責務ではないので、 トークン層で止めています。

幅は決めていません

このシステムには幅の段がありません。 素の dialog は中身に合わせて広がり、 画面幅で止まります。幅を決めたいときは className か style で渡してください。

3つの状態

通常・空・エッジケースの3つは、どのコンポーネントにも必ずあります。

通常default.tsx

通常。開いているかどうかは `useModal` が持つ。

変更を保存しますか

保存しないまま閉じると、書いた内容は消えます。

空empty.tsx

空。中身も操作も無い、見出しだけ。

見出しだけ

エッジケースedge.tsx

エッジケース。中身が長く、入力があり、覆いを押すと閉じる。

送り先を書き足す

覆いを押しても閉じます。中身が画面より高いときは、このダイアログの中だけが巻き取られます—— 後ろは動きません。

props

Prop

Type

型と説明はコンポーネントのソースから抽出しています。既定値は出していません——cva の defaultVariants は 署名に現れないので、表に出すと「引数のデフォルトが書かれているものだけ」になり、嘘になります。

On this page