はじめに
正直なところ、ヘッドレスUIライブラリは群雄割拠の時代なんですよね。Radix UI、Headless UI、Ariakit...色々ある中で、また新しいのが出てきたのかと思っていました。
でもBase UIは、ちょっと出自が違う。Radix UI、Floating UI、そしてMaterial UIの開発者たちが集まって作ったライブラリなんですよ。この組み合わせ、なかなか豪華。
GitHubで6,500スター以上を獲得していて、個人的には「UIコンポーネントの開発経験が詰まった集大成」という印象を受けました。
Base UIとは
Base UIは「Unstyled UI components for building accessible web apps and design systems」と公式で説明されています。日本語にすると「アクセシブルなWebアプリとデザインシステム構築のためのアンスタイルドUIコンポーネント」ですね。
要するに、スタイルが一切付いていない、ロジックだけのコンポーネント集。見た目は自分で100%コントロールできる。
これ、意外と重要なポイントで、既存のデザインシステムに組み込む時に「ライブラリのスタイルを打ち消す作業」が不要になるんですよ。
MITライセンスのオープンソースで、68名以上のコントリビューターが開発に参加。TypeScriptで99.6%書かれているので、型安全性もバッチリです。
特徴・メリット
1. 実績あるチームによる開発
公式サイトで「From the creators of Radix, Floating UI, and Material UI」と謳っているだけあって、UIコンポーネント開発のノウハウが詰まっている印象。
Radixのアクセシビリティ設計、Floating UIのポジショニングロジック、Material UIの大規模運用実績。これらの知見が活かされています。
2. アクセシビリティファースト
WAI-ARIA標準に準拠した実装で、キーボードナビゲーションもしっかり対応。30代になって思うのは、アクセシビリティは「後から対応」だと本当に大変だということ。最初から入っているのは時短になる。
3. 完全にアンスタイルド
スタイルが一切付いていないので、Tailwind CSS、CSS Modules、CSS-in-JS、何でも使える。既存のデザインシステムとの統合がスムーズです。
コスパ的に、スタイルの打ち消し作業がゼロになるのは大きい。
4. 軽量設計
パッケージサイズは141KB(縮小化+Gzip圧縮)。しかもTree-shakingに対応しているので、使っているコンポーネントだけがバンドルに含まれます。
5. コンポーネント構成が柔軟
Base UIのコンポーネントは、複数のパーツを組み合わせて使う設計。これにより、細かい部分まで制御できる自由度があります。
インストール方法
パッケージのインストール
npm i @base-ui-components/react
yarnやpnpmでも同様に。
# yarn
yarn add @base-ui-components/react
# pnpm
pnpm add @base-ui-components/react
初期設定
ダイアログやポップオーバーなどのポップアップコンポーネントを正しく表示するため、ルート要素にスタイルを追加しておくと良いです。
.root {
isolation: isolate;
}
これで、ポップアップが常にページコンテンツの上に表示されるようになります。
iOS Safari対応
iOS Safari向けには、以下のグローバルスタイルも追加。
body {
position: relative;
}
基本的な使い方
Popoverコンポーネント
Base UIでは、コンポーネントパーツを組み合わせて使います。
import { Popover } from '@base-ui-components/react/popover';
export function PopoverDemo() {
return (
<Popover.Root>
<Popover.Trigger className="px-4 py-2 bg-blue-500 text-white rounded">
メニューを開く
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner>
<Popover.Popup className="p-4 bg-white shadow-lg rounded-lg">
<p>ポップオーバーの内容</p>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}
Root、Trigger、Portal、Positioner、Popupといったパーツに分かれていて、それぞれに役割がある。この構成パターンはRadix UIと似ていますね。
AlertDialogコンポーネント
ユーザーの確認が必要なダイアログの例。
import { AlertDialog } from '@base-ui-components/react/alert-dialog';
export function ConfirmDialog() {
return (
<AlertDialog.Root>
<AlertDialog.Trigger className="px-4 py-2 bg-red-500 text-white rounded">
削除する
</AlertDialog.Trigger>
<AlertDialog.Portal>
<AlertDialog.Backdrop className="fixed inset-0 bg-black/50" />
<AlertDialog.Popup className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded-lg shadow-xl">
<AlertDialog.Title className="text-lg font-bold">
本当に削除しますか?
</AlertDialog.Title>
<AlertDialog.Description className="mt-2 text-gray-600">
この操作は取り消せません。
</AlertDialog.Description>
<div className="mt-4 flex gap-2 justify-end">
<AlertDialog.Close className="px-4 py-2 bg-gray-200 rounded">
キャンセル
</AlertDialog.Close>
<AlertDialog.Close className="px-4 py-2 bg-red-500 text-white rounded">
削除
</AlertDialog.Close>
</div>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
);
}
Backdrop、Popup、Title、Description、Closeなど、細かくパーツが分かれているので、レイアウトの自由度が高い。
スタイリングの自由度
Base UIはアンスタイルドなので、好きなスタイリング手法が使えます。
// Tailwind CSS
<Popover.Trigger className="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded">
// CSS Modules
<Popover.Trigger className={styles.trigger}>
// インラインスタイル
<Popover.Trigger style={{ padding: '8px 16px', backgroundColor: 'blue' }}>
個人的にはTailwind CSSとの相性が良いと感じました。クラス名をそのまま書けるので。
実践的なユースケース
デザインシステムの基盤として
Base UIが最も活きるのは、自社のデザインシステムを構築する場面。
// 自社のButtonコンポーネントとして再エクスポート
import { Button as BaseButton } from '@base-ui-components/react/button';
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
}
export function Button({ variant = 'primary', size = 'md', children }: ButtonProps) {
const variantClasses = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
ghost: 'bg-transparent hover:bg-gray-100',
};
const sizeClasses = {
sm: 'px-3 py-1 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
};
return (
<BaseButton className={`rounded ${variantClasses[variant]} ${sizeClasses[size]}`}>
{children}
</BaseButton>
);
}
アクセシビリティのロジックはBase UIに任せて、見た目は自社デザインに統一。この分離がQOL上がりますね。
既存プロジェクトへの段階的導入
全部をBase UIに置き換える必要はなく、特定のコンポーネントだけ使うこともできます。
// ポップオーバーだけBase UIを使う例
import { Popover } from '@base-ui-components/react/popover';
// 他のコンポーネントは既存のまま
import { Button } from './existing-button';
import { Card } from './existing-card';
Tree-shakingのおかげで、使わないコンポーネントはバンドルに含まれない。段階的な移行がしやすい設計です。
複数トリガーの制御
Base UIにはcreateHandleという便利な機能があって、複数のトリガーから一つのダイアログを制御できます。
import { AlertDialog } from '@base-ui-components/react/alert-dialog';
const dialogHandle = AlertDialog.createHandle();
export function MultiTriggerExample() {
return (
<>
<button onClick={() => dialogHandle.open()}>ボタン1</button>
<button onClick={() => dialogHandle.open()}>ボタン2</button>
<AlertDialog.Root handle={dialogHandle}>
{/* ダイアログの内容 */}
</AlertDialog.Root>
</>
);
}
これ、意外とありがたい機能なんですよ。複数箇所から同じダイアログを開きたい場面って結構ある。
Radix UIとの比較
「Radix UIと何が違うの?」という疑問はあると思います。
| 項目 | Base UI | Radix UI |
|---|---|---|
| 開発元 | MUI(元Material UI) | Workos |
| パッケージ | 単一パッケージ | コンポーネント毎 |
| Tree-shaking | 対応 | パッケージ分割で実現 |
| スタイリング | 完全アンスタイルド | 最小限のスタイル付き |
正直なところ、機能的には似ている部分が多い。ただ、Base UIはMUIエコシステムとの親和性を意識した設計になっています。
Material UIから移行を考えている人には、Base UIの方が馴染みやすいかもしれません。
まとめ
Base UIを触ってみた感想:
- 信頼性: 実績あるチームによる開発で安心感がある
- 柔軟性: アンスタイルドなので、どんなデザインにも対応可能
- 軽量: Tree-shakingで必要なものだけバンドル
- アクセシビリティ: 最初から組み込まれているので追加作業不要
- 学習コスト: Radix UI経験者なら低い、そうでなくても直感的
30代になって思うのは、UIライブラリ選びは「今の流行り」より「長期的に使えるか」が大事だということ。Base UIは、MUIチームの長年の知見が詰まっている分、安定性への期待が持てます。
デザインシステムを構築する予定がある人、既存のMaterial UIプロジェクトを軽量化したい人には、検討の価値ありだと思いますよ。