Base UI × Tailwind CSS 実践ガイド: Dialog で学ぶロジックとデザインの分離、Storybook、3 層テスト
モーダルを 1 つ自作すると、見た目とは関係のないコードが増えます。開閉の状態、Esc キーで閉じる処理、開いたときにフォーカスを中へ移す処理、閉じたときに元のボタンへ戻す処理、スクリーンリーダーへ状態を伝える属性。どれも省けませんが、色や余白を決めるコードと同じファイルに混ざっていきます。
ヘッドレス UIヘッドレス UI (Headless UI)見た目を持たず、振る舞いと HTML の構造と支援技術向けの属性だけを提供する UI 部品、またはその部品集。色や余白などのスタイルは使う側が書く。 は、この「見た目とは関係のないコード」だけを部品として提供するライブラリです。このガイドでは、その 1 つである Base UI でお知らせ用のダイアログを作ります。見た目は Tailwind CSS で付け、Storybook に載せて、3 種類のテストで確かめるところまでを通します。
このガイドについて
題材はダイアログ 1 つです。そのダイアログを 2 つの軸で扱います。
- 分ける: 振る舞い (開閉の状態、キーボード操作、フォーカス、WAI-ARIAWAI-ARIAW3C が定めた、HTML の要素に役割・状態・性質を補うための属性の仕様。role 属性と aria- で始まる属性から成り、スクリーンリーダーなどの支援技術に UI の意味を伝える。 の属性) は Base UI に任せ、見た目は Tailwind CSS で書きます。両者をつなぐのは、Base UI が DOM に出す
data-*属性です - 確かめる: 分けた結果を Storybook で見えるようにし、jsdom の単体テスト、Storybook 上のテスト、実ブラウザのテストの 3 層で確かめます。どの検証をどの層に置くかは、08 章で表にまとめます
掲載するコードは、すべて 1 つのプロジェクトに置き、テストか実行で動作を確かめたものです。コードブロックの上に出ているファイル名は、そのプロジェクトの中のパスです。
本ガイドのコードは次の版で動かしています。Base UI は v1.0.0 でパッケージ名が @base-ui-components/react から @base-ui/react に変わりました1。それより前に書かれた記事のコードは import 元が違います。
| 分類 | パッケージ | 版 |
|---|---|---|
| UI ライブラリ | react / react-dom | 19.3 |
| ビルド | vite | 8.3 |
| 言語 | typescript | 6.0 |
| ヘッドレス UI | @base-ui/react | 1.8 |
| スタイル | tailwindcss / @tailwindcss/vite | 4.3 |
| カタログ | storybook | 10.6 |
| テスト | vitest / vitest-browser-react | 5.0 / 2.3 |
| テスト | @testing-library/react / @testing-library/user-event | 16.3 / 14.6 |
| テスト | jsdom | 30.1 |
| ブラウザの操作 | playwright / @playwright/test | 1.63 |
| 検証 (09 章) | zod | 4.6 |
| データ取得 (09 章) | @tanstack/react-query | 5.104 |
Node.js は 24 を使っています。
Storybook の導入コマンドが入れるパッケージの版は、実行する時点で変わります。導入し直して確かめたときは Vitest 4.1 と Playwright 1.64 が入り、その版でも本ガイドのテストがすべて通りました。版の扱いは 04 章で説明します。
対象読者
- React と Tailwind CSS でコンポーネントを書いたことがあり、ヘッドレス UI はまだ使ったことがない人
- モーダルやドロップダウンを自作して、状態やアクセシビリティのコードが膨らんだ経験がある人
- Storybook とコンポーネントのテストについて、何をどの道具でどこまで書くかを決めかねている人
前提知識
| 必須度 | 知識 | 補足 |
|---|---|---|
| 必須 | React で関数コンポーネントと props を書ける | TypeScript で書きます |
| 必須 | Tailwind CSS のクラスで余白や色を指定したことがある | hover: のような variant を使ったことがあれば足ります |
| 推奨 | テストを 1 本でも書いて動かしたことがある | Vitest でなくても構いません |
| 不要 | Storybook、Playwright、ヘッドレス UI の経験 | 導入から扱います |
既存記事との棲み分け
このガイドは既存のガイドの続きに当たります。重なる内容は書かず、次の表の参照先へ送ります。
| 観点 | 参照先 | 本ガイドが持つもの |
|---|---|---|
| モーダルを素の React で自作する | PortalとModal | 同じ振る舞いを Base UI に任せたときの書き方と、任せた部分の確かめ方 |
| Tailwind CSS の導入と基本のクラス | Tailwind CSS ガイド | data-* 属性の variant と、開閉に合わせた遷移 |
cva と cn、Radix UI の asChild | shadcn/ui Button で学ぶデザインシステム設計 | Base UI の render prop による合成 |
| 役割で要素を取るテストの考え方 | 振る舞いをテストする | ヘッドレス UI を対象にした 3 層の使い分け |
| Vitest の導入とマッチャー | 開発ツールチェーン | jsdom と実ブラウザを 1 つの設定で併用する方法 |
章立て
全 9 章を 4 つの部に分けています。
第 1 部 ロジックとデザインを分ける
| 章 | 扱うこと |
|---|---|
| 01 なぜ Base UI と Tailwind CSS を組み合わせるのか | 自作のモーダルが抱える仕事と、ヘッドレス UI がそのうちどこを引き受けるか |
| 02 ロジック層とデザイン層 | 2 つの層の責務と、両者をつなぐ data-* 属性 |
| 03 Dialog を実装する | お知らせダイアログの実装。状態も ARIA も書かずに済む理由 |
第 2 部 カタログにする
| 章 | 扱うこと |
|---|---|
| 04 Storybook を導入する | 導入、Tailwind CSS の適用、a11y アドオン、CSF 3 での story の書き方 |
第 3 部 3 層でテストする
| 章 | 扱うこと |
|---|---|
| 05 役割でテストする | クラス名でなく役割で要素を探せる理由。純粋関数への切り出し。3 層の全体像 |
| 06 単体テスト | Vitest と React Testing Library で、開閉とフォーカスを jsdom の上で確かめます |
| 07 Storybook でテストする | play 関数、a11y の自動検査、Chromatic による見た目の差分検知 |
| 08 実ブラウザでテストする | Vitest Browser Mode と Playwright。どの検証をどの層に置くかの表 |
第 4 部 まとめ
| 章 | 扱うこと |
|---|---|
| 09 まとめと次の一歩 | 4 つの分担の振り返りと、フォームやデータ取得へ広げるときの分け方 |
このガイドで扱わないこと
| 観点 | 参照先 |
|---|---|
| Dialog 以外の部品 (Menu、Select、Tooltip など) | Base UI の公式ドキュメント |
| デザイントークンと、variant を持つ部品の設計 | shadcn/ui Button で学ぶデザインシステム設計 |
| テストを先に書く進め方 | TDD 実践ガイド |
| Next.js などでのサーバーサイドレンダリング | 本ガイドの範囲外です |
| CI への組み込みと、Chromatic の運用や料金 | 本ガイドの範囲外です |
読み方
順に読むと、1 つのダイアログが実装され、カタログに載り、テストで守られるところまで進みます。
テストだけに関心がある場合は 05 章から読めます。その場合も、テストの対象になる 03 章のコードには目を通してください。Storybook の導入だけを知りたい場合は 04 章が独立して読めます。