WebでGoogleマップのようなハーフモーダルを実装する
Vaul とは
「ハーフモーダル」という UI パターンをご存知でしょうか?
モーダルウィンドウの一種で、画面の下半分を覆うように表示される UI パターンです。iOS 15 で導入されてから、スマートフォンではよく見かけるようになりました。
Customize and resize sheets in UIKitより
ハーフドロワーや半モーダルといった呼び方もあるようです。
Vaul はこのようなハーフモーダルを React で実装するためのライブラリで、スタイルが提供されていないヘッドレスではありますが様々なカスタマイズができるようになっています。
Radix UI の Dialog コンポーネントをベースに作られているため、Radix UI で利用可能な APIをそのまま利用することができます。
基本的な実装
Vaul を使って、基本的なハーフモーダルを実装してみます。
まずは npm から Vaul をインストールします。
npm install vaul
Vaul のコンポーネントを使ってハーフモーダルを実装します。
"use client";
import { useState } from "react";
import { Drawer } from "vaul";
const HalfModal: React.FC = () => {
return (
<Drawer.Root shouldScaleBackground>
<Drawer.Trigger asChild>
<button className={styles.drawer_trigger}>Open Drawer</button>
</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay className={styles.drawer_overlay} />
<Drawer.Content className={styles.drawer_content}>
<div className={styles.drawer_inner}>
<div className={styles.handle} />
<div className="max-w-md mx-auto">
<Drawer.Title className={styles.title}>
This is half drawer
</Drawer.Title>
</div>
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
};
export default HalfModal;
.drawer_trigger {
background-color: #03a9f4;
color: white;
border-radius: 0.5rem;
font-size: 1.5rem;
padding: 0.5rem 1rem;
position: fixed;
bottom: 1.5rem;
right: 1rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.5);
z-index: 1001;
transition: all 0.3s;
&:hover {
background-color: #0288d1;
}
}
.drawer_overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.4);
z-index: 1001;
}
.drawer_content {
background-color: white;
border-radius: 1rem 1rem 0 0;
height: 30%;
margin-top: 6rem;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1002;
}
.drawer_inner {
padding: 1rem;
background-color: white;
border-radius: 1rem 1rem 0 0;
flex: 1;
}
.handle {
margin-left: auto;
margin-right: auto;
width: 3rem;
height: 0.375rem;
flex-shrink: 0;
border-radius: 9999px;
background-color: #d4d4d8;
margin-bottom: 2rem;
cursor: pointer;
}
ページコンポーネントに組み込み、ローカルサーバーで確認してみます。
npm run dev
Vaul によるハーフモーダルの実装例 1
最低限のハーフモーダルを実装することが出来ました。
モーダルとモードレス
さて、ここで 現在の UI と Google マップで採用されている UI を比較してみます。
Google マップで東京駅を表示
モーダルウィンドウは、ユーザーがそのウィンドウを閉じるまで他の操作を行えないようにする UI パターンです。モーダルウィンドウが表示されている間は、モーダルウィンドウ以外の UI は操作できないようになります。先ほど実装したのはこちらの UI パターンです。
一方でモードレスウィンドウは、ユーザーがそのウィンドウを閉じるまで他の操作を行えるようにする UI パターンです。モードレスウィンドウが表示されている間も、モードレスウィンドウ以外の UI は操作できるようになります。今回実装したいモバイル版 Google マップで採用されている UI は、モードレスウィンドウの一種です。
このため、Google マップの UI は厳密にはハーフモーダルとは異なるのですが、ネット上の記事を見ると Google マップの UI をハーフモーダルに括っているものも多いようなので、ここでも便宜的にハーフモーダルと呼ぶことにします。
Google マップのような UI を実装する
実際に Vaul を使ってハーフモーダルを実装してみます。
グローバルに適用している CSS ファイルで、以下のようなスタイルを追加します。
body {
pointer-events: auto !important;
}
多少 hack 的な方法であることは否めませんが、Vaul がbody
に対して指定しているpointer-events: none;
を上書きすることで、このような UI を実装することができます。
実は Vaul にはbody
へのスタイル指定を無効化するためのnoBodyStyles
という prop が用意されているのですが、なぜかpointer-events
に対する指定だけは無効化されないようです。これが仕様通りの挙動なのかは分かりませんが、いまいち用途が分からないので普通にバグではないかと疑っています。軽く Issue もチェックしたんですが、同じように悩んでいそうな人がいた。
次に、先ほど書いた 2 つのファイルを以下のように修正します。
"use client";
import { useState } from "react";
import { Drawer } from "vaul";
import styles from "./HalfModal.module.css";
const HalfModal: React.FC = () => {
const [snap, setSnap] = useState<number | string | null>("100px");
return (
<Drawer.Root
activeSnapPoint={snap}
disablePreventScroll={false}
open={true}
setActiveSnapPoint={setSnap}
snapPoints={["115px", "240px", "480px", 0.9]}
>
<Drawer.Trigger />
<Drawer.Portal>
<Drawer.Content className={styles.content}>
<div className={styles.inner_content}>
<div className={styles.handle} />
<div>Main Content</div>
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
};
export default HalfModal;
Vaul では snapPoints
というプロパティを使ってモーダルのスナップポイントを指定することができます。この配列にはピクセル値だけでなく 0 から 1 までの間でパーセンテージによる指定をすることもできます。
.content {
background-color: #f4f4f5;
display: flex;
flex-direction: column;
border-top-left-radius: 1rem;
border-top-right-radius: 1rem;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1001;
height: 100%;
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5);
}
.inner_content {
padding: 1rem;
background-color: white;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
flex-grow: 1;
}
.handle {
margin: 0 auto;
width: 3rem;
height: 0.375rem;
border-radius: 9999px;
background-color: #d4d4d8;
margin-bottom: 1rem;
cursor: grab;
}
これで以下のようなハーフモーダルを実装できるはずです。
Vaul によるハーフモーダルの実装例 2
おわりに
Vaul では他にも様々な APIが提供されているので、かなり柔軟にカスタマイズすることができます。ヘッドレスなライブラリなのでスタイルは自分で書く必要がありますが、公式が作例を用意してくれているので、それをそのまま使ってしまうのもアリだと思います。
ハーフモーダルやこれに準じた UI を実装する際はぜひ試してみてください。
最後に、今回 Vaul を採用したプロジェクトのリポジトリと、その他参考にした記事をリンクしておきます。
Discussion