Open8

VaulというReactライブラリが最高すぎる

masa5714masa5714

Vaul公式サイト

名前覚えらんない。4文字なのに絶妙にムズい名前。

2024年9月27日にv1.0.0がリリースされたばかり。おしゃれで実用的なトースト(スナックバー)のSonnerを作った人が作っている。

https://vaul.emilkowal.ski/

何ができるのか?

Googleマップなどのネイティブアプリにある下から出てくる モーダルシート を簡単に実装できるライブラリです。モーダルシートだけではなく、スワイプで閉じれるドロワメニュー にも使えます。

余計な装飾が全くついておらずかなり扱いやすくカスタマイズしやすいです。

サンプルはこちら

https://vaul.emilkowal.ski/default

▼個人開発中のWebアプリケーションに実装してみた感じ

冒頭チラついてるのは動画カットミスの影響。ブラウザ上は何ら変な動きはありません。

masa5714masa5714

実装方法はシンプル

import { Drawer } from "vaul";
import { useState } from "react";

function MyComponent () {
  const { Root, Portal, Content, Overlay } = Drawer;
  const [isOpen, setOpen] = useState(false);

  return (
    <Root open={isOpen} onClose={() => setOpen(false)} direction="bottom">
      <Portal>
        <Overlay className="fixed inset-0 z-40 bg-black/70" />
        <Content className="fixed bottom-0 left-0 w-full bg-white z-50 rounded-t-[10px] outline-none">
          <p>hoge</p>
          <p>hoge</p>
          <p>hoge</p>
          <p>hoge</p>
          <p>hoge</p>
        </Content>
      </Portal>
    </Root>
  );
}

すごく直感的に実装できますね。
サンプルコードでは className="" の表記が無く、実装方法に迷ってしまいましたが、上記のように fixed を付けるのはほぼ必須です。 2024年12月8日時点のドキュメントにはclassNameの付いた例が掲載されていました!

<Overlay className="fixed inset-0 z-40 bg-black/70" /> では、背景膜を作るための記述です。

direction="bottom" は下から出てくる動きを想定していることをコンポーネントに伝えるものです。ドロワメニューで左から出てくるものを作る場合は、 direction="left" と指定できます。閉じるスワイプ方向はここで指定する形となります。

今後の定番ライブラリになりそう!

めちゃくちゃ便利。
もはやドロワメニュー自分で実装する理由が一切無いとすら感じます。
定番ライブラリになると思うので必ずチェックしておいた方が良いでしょう。

masa5714masa5714

スナップポイントも指定できる

スナップポイントも指定できます。
スナップポイントとは、表示領域に節を指定するようなイメージです。例えば、最初はタイトルだけ表示するが、少し上にスワイプするとコンテンツが出てくるみたいな動きを簡単に実現できます。

モーダル内のスクロールにも対応OK

記述はだいぶ端折りますが下記の記述がポイントです。

<Content className="h-4/5">
  <div className="overflow-y-auto h-full">
    <p>hoge</p>
    <p>hoge</p>
    <p>hoge</p>
    <p>hoge</p>
    <p>hoge</p>
    <p>hoge</p>
    <p>hoge</p>
    <p>hoge</p>
    <p>hoge</p>
    <p>hoge</p>
  </div>
</Content>

<Content> に対して h-4/5 でモーダルシートの高さを固定しておき、下のdivタグに表示領域の高さを指定した上で overflow-y-auto によってコンテンツ量が多ければスクロール許可する形になります。

この基本形だけ知っておけば特に実装に困ることは無いでしょう。

masa5714masa5714

overlayやスワイプによるクローズを無効にする

<Root dismissible={false}> のように dismissible={false} を false にするだけで実現できる。誤操作が致命的になりそうな重要な文言を必ず見せたいときなどに活用できそうだ。同意ボタンとか。

masa5714masa5714

使われている事例を見つけた

運営母体がオンカジのグレーな配信サイト「KICK」
最近、横山緑が活動拠点にしたことで注目を集めている。

https://kick.com/yokoyamamidori

スマホ、タブレット表示のときのギフトランキングの表示で使われている。


下記のWebアプリでも使われてる模様。
PWAをガッツリ導入してて最先端すぎる。すごい。
※ちなみにポーカーには全く興味ありません。たまたま見つけました。

https://pokerqz.com/

木次大樹木次大樹

POKER Q'zの開発責任者をしているものです。
ちょうどvaulの仕様について調べていたところ、こちらの記事を見つけて、なんとアプリの紹介をされているのを見つけてしまいました。笑 ポーカーに全然興味ないとのことですが、ご紹介いただきありがとうございます。

dismissible={false} だと スクロール + overlayの無効化になると思うのですが、どちらかのみ制御ってできるんですかね?(ex. overlayのタップのみ無効)🤔

masa5714masa5714

すみません、かなり遅くなりました🙇‍♂️

現在はWeb版としての提供を終了されているようですが、当時、AndroidのPlayストアでPWA配信されている動く事例を初めて見たことと、とてもWeb技術だけで作られたとは思えない、ネイティブアプリ風味を高次元で再現されていて完成度に感動しました!それと同時にPlayストアでのPWA配信の現実を見てしまったというか、課金周りの実装がWeb用(Stripe)とアプリ用(Play Billing)で分けないといけなくて大変そうだなぁと興味深く、非常に有益な学びをさせて頂きました。

dismissible={false} について、僕の知る限りでは片方だけを有効・無効にする手段は無いと思いいます。ただし、擬似的に力技で実現できなくもないかもしれません。(試してないので実現に際してなにか問題が起きるかもしれないです......。)

1. スワイプで閉じれるが、overlayタップは無効にする手段

背景色のない <Content> が画面全体を覆うようにして、その中に背景色付きのコンテンツを設置する。<Content> が邪魔して Overlayに触れなくなるため擬似的なoverlay無効が実現できるかもしれません。副作用としてスワイプ領域が画面全体になってしまいますが。

2. スワイプでは閉じれないが、overlayタップだけは有効にする手段

dismissible={false} を付与してどちらも無効にした上で、 <Overlay> に対して onClick で閉じる <Root> の開閉状態を変更できるイベントを追加する。

こんな感じでしょうか。
根本的な解決策を提案できなくて申し訳ないです!

木次大樹木次大樹

ご返信いただきありがとうございます!
PWAにしてはかなりネイティブに頑張って寄せているので、その点を評価いただき非常に嬉しいです!ただ仰る通り、課金周りの件や、現状ストア配信の方がレビュー実績やチャネルの観点でビジネス価値が積めるのでよほどのことがない限りFlutterやReact Nativeなどで開発した方がまだまだ体験は良いだろうな…とは感じています笑

またご質問についてもご回答いただきありがとうございます!
本件は解決はしたのですが、ネストでDrawerを表示させるといくつか不具合が出るようです(開発環境だと再現できずデプロイ環境だと再現する件だったので、かなり面倒くさい事象でした…)。

備忘録的に記載しておきます。

発生した問題と対処法

1. pointer-eventsの問題

  • 現象: body部に pointer-events: none が適用されて、閉じても画面が動かなくなる(単体のDrawerだと発生しないが複数時に発生?この辺は不明)
  • 対処: 現状仕様らしく、下記Issue内で提案されているworkaroundで回避は可能

2. ネストDrawerの同時クローズ問題

  • 現象: ネストでdrawerを開いている際に、前面のDrawerを閉じると背面のDrawerも同時に閉じてしまう
  • 対処: drawerの open={main && !sub} で片方が閉じていることを明示的に示す必要あり

参考資料