より良いユーザー体験を求めて "モーダル" を深掘る
モーダルと聞いてどんなUIを想像しますか?
iOSを使ってる人はこのようなUIを想像するのではないでしょうか。(iOS26では見た目が変わりましたね)
実はモーダルというUIコンポーネントは存在しません。
このようなUIはHIGではシートと呼んでいます。
では「モーダル」は本来どう言う時に使えば良いのでしょうか?
この記事ではモーダルが表すUIについて種類や実装方法などを深掘り、より良いユーザー体験を実現する方法を見ていきます。
読者対象は、Flutterエンジニアはもちろん、モバイルエンジニア全般やFデザイナーにも読んでもらいたいです。
モーダルとは
「モーダル」というのは、ユーザーを特定のタスクに集中させるためにUIに「モード」を持たせているUIのことを指します。
先ほどのシートは入力画面などで使われますが、タスクを完了するかキャンセルするまでは他の操作はできません。
リマインダーアプリの作成画面
また、ダイアログも同様に操作を制限します。
リマインダーアプリの削除ダイアログ
先ほど説明した通り、「モーダル」という具体的なUIコンポーネントは存在しません。
ということは「モーダルで実装しておいて!」というのは具体性に欠けています。
実際には「モーダルシートで実装しておいて!」が正しそうです。
ここからは、モーダル表示を行うためのUIの種類と実装方法を見ていきます。
フルスクリーンビュー
Flutterを使っているとよく使うページではないでしょうか。
画面下から表示され、キャンセルボタンや完了ボタンを押すことで閉じることができます。
FlutterではfullscreenDialog: true
を設定するだけです。
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const FullscreenPage(),
fullscreenDialog: true,
),
);
Bottom Sheet
こちらも同様によく使うUIですが、フルスクリーンと異なる点はシートを下にスワイプしたり外側をタップすることで閉じれる点です。
そのためフルスクリーンに比べてより軽い選択や操作をさせる時に採用するケースが多い印象です。
元の画面に意識を向けつつモーダル表示をしたい場合に使われます。
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return ...
},
);
CupertinoSheet
こちらはiOSユーザーにはお馴染みのUIかと思います。
Flutterでは割と最近(3.29あたり?)に実装されました。
ただ、残念ながらiOS26になってからだいぶ見た目が変わってしまいましたね😭
showCupertinoSheet<void>(
context: context,
builder: (BuildContext context) => Container(
height: MediaQuery.of(context).size.height * 0.6,
color: CupertinoColors.systemBackground,
child: const SizedBox(),
),
);
ドラッグできるSheet
ボトムシートににていますが、スクロールするとシートが大きくなるようなことも実現できます。
iOSの共有画面とかで使われていました。(これまたiOS26で変わってしまいましたが)
FlutterではshowModalBottomSheet
とDraggableScrollableSheet
を組み合わせます。
showModalBottomSheet(
context: context,
isScrollControlled: true, // フルスクリーン近くまで広げられる
builder: (context) {
return DraggableScrollableSheet(
initialChildSize: 0.5, // 初期の高さ(画面に対する割合)
minChildSize: 0.3, // 最小の高さ
maxChildSize: 0.9, // 最大の高さ
expand: false, // trueにすると常に全画面に展開される
builder: (context, scrollController) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: ListView.builder(
controller: scrollController, // ← これを渡すのが重要
itemCount: 30,
itemBuilder: (context, index) {
return ListTile(
leading: const Icon(Icons.label),
title: Text('アイテム $index'),
);
},
),
);
},
);
},
);
Sheetはモーダルビューなのか?
FlutterのshowModalBottomSheet
とかはモーダルビューですが、ボトムシート全てがモーダルビューとは限りません。
例えばボトムシートが表示されながらも下の画面を操作できるのであれば、モーダル(特定の操作を限定している)わけではありません。
シートが必ずしもモーダルビューではないことはMaterialDesignやHIGにも記載されています。
具体例としてはiOSであればメモ帳やMapアプリで採用されていますね。
Mapアプリ
メモ帳
ボトムシートを表示しながら、下の画面を操作できるような実装はFlutterではScaffold.of(buttonContext).showBottomSheet
を使います。
showBottomSheetを使った例
ElevatedButton(
onPressed: () {
Scaffold.of(buttonContext).showBottomSheet(
(ctx) => Container(
height: 200,
color: Colors.blue,
child: const Center(
child: Text(
'Bottom Sheet',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
);
},
child: const Text('Show BottomSheet'),
);
ダイアログを使わない
ボトムシート以外のモーダルビューにダイアログがあります。
ユーザーに確認をとる時に使い、実装も簡単なので多用しがちだと思います。
ただし、ダイアログを使っているとき、それはモードが発生しているため、ユーザーの行動を制限していることになります。
不用意に使ってしまうとストレスの多い体験になってしまいます。
削除を確認するためのダイアログ
逆に、モードを発生させないことでより良いユーザー体験を目指すにはどうすれば良いでしょうか。
このようなモードを発生させない状態をモードレスといいます。
Snackbar
削除系の操作を行う場合、誤操作を防ぐために確認ダイアログを表示するケースがあると思います。
そんな時に代替案として使える例として、Snackbarなどで取り消しボタンを表示します。
Snackbarを表示している間でもユーザーはそれ以外の操作を行うことができます。
ゴミ箱
UIというより機能面の話ですが、削除したアイテムをゴミ箱に表示する方法も考えられるかと思います。
メールアプリや写真アプリなどで見かけますね。
もちろん、取り消しもゴミ箱も工数はかかりますし、破壊的な変更の場合はダイアログの方が適している場面もあります。
まとめ。
色々なシートや、ダイアログの代わりになるものを見てきました。
自分の記事全てにおいて言えることなんですが、エンジニアが実装可能なUIの幅を広げることで、デザイナーに提案できるし、ユーザー体験をあげることに関われることができると思います。
ぜひ他の記事も読んでいただけたら嬉しいです。
また、良いと思ったらいいねもらえると次の記事へのモチベーションになります。❤️
参考

株式会社SODAの開発組織がお届けするZenn Publicationです。 是非Entrance Bookもご覧ください! → recruit.soda-inc.jp/engineer
Discussion