はじめに
スマートフォンでアプリを操作していると、画面下からニュッと出てくるメニューやアクションシートを目にすることが多いのではないでしょうか。あの「下から生える」UIをReactでゼロから実装しようとすると、ドラッグでの開閉、慣性を考慮したアニメーション、背景のスケーリング演出など、思った以上に作り込みが必要になります。
Vaulは、そんなモバイル向けドロワーUIを驚くほど簡単に実装できるReactライブラリです。今回はVaulの特徴と使い方を、実際のコード例とともに紹介します。
Vaulとは
VaulはEmil Kowalski氏が開発した、スタイルを持たない(unstyled)Reactのドロワーコンポーネントです。内部ではRadix UIのDialogをベースにしており、アクセシビリティを担保しながらタブレットやモバイル端末向けのDialog代替として利用できるよう設計されています。shadcn/uiのコンポーネントとしても採用されており、多くのプロダクトで実績のあるライブラリです。
主な特徴
- スタイルフリー - CSSは一切バンドルされておらず、Tailwind CSSなど任意のスタイリング手法と組み合わせて自由にデザインできる
- ネイティブに近い操作感 - ドラッグでの開閉やスワイプでの閉じる動作が標準で実装されており、慣性の効いた自然なアニメーションが得られる
- Snap Pointsに対応 - ドロワーを途中の高さで止める「スナップポイント」を指定でき、地図アプリのようなUIも実現できる
- Nested Drawer対応 - ドロワーの中からさらに別のドロワーを開く、多段階のUIフローを構築できる
- 方向指定が可能 -
directionプロパティで下・上・左・右のいずれからドロワーを出現させるか指定できる
インストール
npmでインストールします。
npm install vaul
yarnやpnpmを使う場合は以下の通りです。
yarn add vaul
pnpm add vaul
基本的な使い方
VaulはDrawer.Rootを起点に、Trigger・Portal・Content・Overlayなどのサブコンポーネントを組み合わせて使います。
import { Drawer } from 'vaul';
function MyDrawer() {
return (
<Drawer.Root>
<Drawer.Trigger asChild>
<button className="rounded bg-black px-4 py-2 text-white">
メニューを開く
</button>
</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
<Drawer.Content className="fixed bottom-0 left-0 right-0 flex flex-col rounded-t-2xl bg-white p-4">
<div className="mx-auto mb-4 h-1.5 w-12 rounded-full bg-gray-300" />
<Drawer.Title className="text-lg font-semibold">
設定メニュー
</Drawer.Title>
<p className="mt-2 text-sm text-gray-500">
ここに任意のコンテンツを配置できます。
</p>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
}
Drawer.Contentにはドラッグで閉じる操作が自動で組み込まれるため、開閉ロジックを自前で書く必要はありません。Drawer.Overlayをクリックした場合も、デフォルトでドロワーが閉じます。
実践的なユースケース
スナップポイントでUIの高さを制御する
ECサイトの商品詳細やフィルターメニューのように、「少しだけ見せる」「全画面に広げる」を切り替えたい場合はsnapPointsが便利です。
import { useState } from 'react';
import { Drawer } from 'vaul';
const snapPoints = ['148px', '355px', 1];
function ProductFilterDrawer() {
const [snap, setSnap] = useState(snapPoints[0]);
return (
<Drawer.Root
snapPoints={snapPoints}
activeSnapPoint={snap}
setActiveSnapPoint={setSnap}
>
<Drawer.Trigger asChild>
<button className="rounded bg-black px-4 py-2 text-white">
絞り込み
</button>
</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
<Drawer.Content className="fixed bottom-0 left-0 right-0 rounded-t-2xl bg-white p-4">
<Drawer.Title className="text-lg font-semibold">
絞り込み条件
</Drawer.Title>
<p className="mt-2 text-sm text-gray-500">
ドラッグして表示エリアを調整できます。
</p>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
}
snapPointsにはピクセル値の他、1のように割合(画面全体)も指定できます。ユーザーがドラッグ中の位置に応じて、指定したスナップポイントへ自動的に吸着します。
ネストしたドロワーでカート→決済フローを作る
カートの中身を確認するドロワーから、そのまま決済確認のドロワーへ進むといった多段階フローにはDrawer.NestedRootが使えます。
import { Drawer } from 'vaul';
function CartDrawer() {
return (
<Drawer.Root>
<Drawer.Trigger asChild>
<button className="rounded bg-black px-4 py-2 text-white">
カートを見る
</button>
</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
<Drawer.Content className="fixed bottom-0 left-0 right-0 rounded-t-2xl bg-white p-4">
<Drawer.Title className="text-lg font-semibold">カート</Drawer.Title>
<Drawer.NestedRoot>
<Drawer.Trigger asChild>
<button className="mt-4 rounded bg-black px-4 py-2 text-white">
決済に進む
</button>
</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
<Drawer.Content className="fixed bottom-0 left-0 right-0 rounded-t-2xl bg-white p-4">
<Drawer.Title className="text-lg font-semibold">
決済確認
</Drawer.Title>
</Drawer.Content>
</Drawer.Portal>
</Drawer.NestedRoot>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
}
Drawer.NestedRootを使うと、外側のドロワーが自動的に少し縮小し、内側のドロワーが手前に重なるアニメーションが得られます。ユーザーは「今どの階層にいるか」を視覚的に把握しやすくなります。
まとめ
Vaulは、モバイルでよく見かける「下から生えるドロワー」のUX(User Experience:ユーザー体験)を、最小限の実装で再現できるReactライブラリです。スタイルを持たない設計のため既存のデザインシステムに馴染みやすく、Snap PointsやNested Drawerといった実務で使える機能も揃っています。
まずはシンプルなDrawer.Root構成から試してみて、慣れてきたらスナップポイントやネスト構造を取り入れ、アプリのUXを一段引き上げてみてください。
