⚫️

祝v1リリース🎉新たなヘッドレスUIライブラリBase UIが良さそう

に公開

この記事は React Advent Calender 2025 18 日目の投稿です。

(追記履歴)

  • 2026/01/17 初版の投稿
  • 2026/01/25 shadcn/ui で従来使用されてきた shadcn@latest add コマンドでも対応したことを追記

はじめに

alpha 版時代から個人的に注目していた React 専用のヘッドレス UI ライブラリ Base UI が v1 リリースされました 🎉!
これがかなり良さそうなので今回紹介しようと思います!

https://x.com/base_ui/status/1999154611123257522

元々は MUI 発のライブラリ

GitHub リポジトリ名で察する方も多いかもしれませんが、元々 MUI(Material UI)の姉妹ライブラリとして開発されていた MUI Base というライブラリが始まりです。Unstyled な MUI として開発が進められていましたが、Material Design の適用などの問題から完全に独立したライブラリとして方針転換を図ることになりました。

https://v6.mui.com/base-ui/getting-started/

方針転換後は、MUI チームに加え、RadixUIFloating UI[1]のクリエイターも加えた新たな体制で再スタートを切ることになりました。

https://base-ui.com/react/overview/about

同じヘッドレス UI ライブラリ Radix Primitives のメンテナンス頻度低下の議論[2]が起こる中、新たなライブラリとして海外コミュニティを中心に徐々に注目を集めるようになりました。

Base UI の特徴

導入コストの軽さ

MUI 発と言われると重い導入コストを想起されるかもしませんが、それを一切感じさせないコストとなっています。特に外部ライブラリを入れる必要もなく Base UI のみで解決します。

https://base-ui.com/react/overview/quick-start

iOS 26 以降のみ、Safari 対応による body タグの編集こそ必要なものの全体的には比較的軽いものとなっています[3]

上記の設定後は、スタイリング時に事前の設定は一切不要です。
https://base-ui.com/react/handbook/styling

公式ドキュメントの豊富な実装例

ドキュメントに関しても v1 ながらすでに手の込んだ完成度になっています。CSS Modules/Tailwind CSS 両方のコード例が記載されていたり、各パーツの説明もわかりやすい印象です(どの HTML タグを示しているのかの説明まであります)。

例に関しても実装する機会が多いパターンは網羅されている印象です。その中でもいくつか手の込んだ例もあり、CheckboxGroup コンポーネントのページでは、ネストしたチェックボックスの実装例まで紹介されていたのは驚きました。
https://base-ui.com/react/components/checkbox-group#examples

Render function アプローチによる子要素の表示

RadixUI などが採用する asChild アプローチに対して、BaseUI では Render function アプローチを採用することによって型安全性の確保を実現しています。

https://base-ui.com/react/handbook/composition#render-function

asChild アプローチが抱える課題に関しては以下の記事が詳しいのでぜひ読んでみてください。
https://zenn.dev/tsuboi/articles/8abddb1ae3038f

AutoComplete/Combobox のサポート

shadcn/ui や他のヘッドレス UI ライブラリで検索フォームを作ったとき、AutoComplete/Combobox コンポーネントの対応がなく悩んだ方も多いかもしれません。

いずれのコンポーネントも実装コストの高さからライブラリでの対応が避けられがちなのですが、Base UI では標準でサポートされています。

https://base-ui.com/react/components/autocomplete
https://base-ui.com/react/components/combobox

さらにタグ形式の表示のサポートや、useFilterによる絞り込み hooks も完備されています。

llms.txt ファイルのサポート

現在のトレンドに合わせて、llms.txt も公開されています。これにより AI ツールで参照させることも容易な環境が整っています。

https://base-ui.com/llms.txt

実装サンプル

テキスト紹介だけだと使い勝手が伝わりづらいと思うのでサンプルをここで掲載します。

※今回は Vite の React テンプレートおよび Tailwind CSS を使って作成します。

インストールと初期設定

React アプリにまずはインストールを行います。現状は全コンポーネントが入ったパッケージしか公開されていないため注意が必要です。

npm i @base-ui/react

そしてアプリの body タグおよび直下の div タグに Global な CSS を追加する必要があります。

今回サンプルに使用した Vite のテンプレートでは以下の構成となっています。

index.html
<body>
  <div id="root"></div>
</body>

そのため、Global な CSS が定義されている index.css に以下を当てます[4]

index.css
/* for iOS26+ Safari。対応する場合だけ記載が必要 */
body {
  position: relative;
}

#root {
  isolation: isolate;
}

これで初期設定は完了です。

まずは Popover を組んでみる

CSS を変更したので動作確認のため、まず Popover コンポーネントの動作を確認したいと思います。

Popover のサンプルコードはこちらです。

import type { FC } from "react";
import { Popover } from "@base-ui/react/popover";
import { ArrowIcon } from "./icon/ArrowIcon";

export const AppPopover: FC = () => {
  return (
    <Popover.Root>
      <Popover.Trigger className="flex p-2 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900">
        テストPopover
      </Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner sideOffset={8}>
          <Popover.Popup className="rounded-lg bg-[canvas] px-6 py-4 text-gray-900 shadow-lg shadow-gray-200 outline-1 outline-gray-200">
            <Popover.Arrow className="data-[side=bottom]:-top-2">
              <ArrowIcon />
            </Popover.Arrow>
            <Popover.Title className="text-base font-medium">
              通知
            </Popover.Title>
            <Popover.Description className="text-base text-gray-600">
              Popoverが出ました
            </Popover.Description>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
};
ArrowIcon コンポーネントの内容。
import type { ComponentProps, FC } from "react";

export const ArrowIcon: FC<ComponentProps<"svg">> = (props) => {
  return (
    <svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
      <path
        d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
        className="fill-[canvas]"
      />
      <path
        d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
        className="fill-gray-200"
      />
      <path d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z" />
    </svg>
  );
};

Popover.Arrow コンポーネントだけ以下意識しておく必要があります。

  1. Popover.Arrow コンポーネントだけでは矢印アイコンが表示されず、自前の svg タグを用意&使用する必要がある。
  2. data-side の値によって表示位置をスタイリングする必要があり、デフォルトは data-side=bottom となる。

https://base-ui.com/react/components/popover#arrow

これで Popover が使用できることを確認できました。

完成したPopoverのサンプル

検索フィルターフォーム UI のサンプル

サンプルとしてもう 1 つ、検索フィルターフォームを想定した UI も載せておきます。

今回設置するパーツとしては以下を使用します。

  • テキスト入力欄
  • チェックボックス
  • コンボボックス

実装に必要なコンポーネントを整理

まずは Form コンポーネントでフォームの大枠を作ります。
実際のフォームでは入力内容の Submit もここで行います。
https://base-ui.com/react/components/form

次に各入力項目を示す、Field コンポーネントです。
https://base-ui.com/react/components/field

Field.Root タグ配下に以下のタグを置くことでフォーム項目が作れます。

  • Field.Label: 項目の label タグ
  • input タグを示す Field.Control もしくはそれ以外のコンポーネント

今回は使用しませんが、他にもエラーメッセージを置く Field.Error タグ、注釈を置く Field.Description も存在します。

次はチェックボックスには Checkbox コンポーネントを使います。
https://base-ui.com/react/components/checkbox

最後のコンボボックスには前述した Combobox コンポーネントを使います。今回は 1 つだけ選択するパターンで作ります。

各パーツの実装

まずテキスト入力欄ですが、Field.Control タグがそのまま input タグになるため、以下の感じで記載すれば OK です。

<Field.Control type="text" placeholder="山田 太郎" required />

次は一旦飛ばして、チェックボックスになります。ここはタグも少ないため、以下の構成を見れば理解しやすいかと思います。
今回は単純なチェックのみですが、Checkbox.Root では state などの値を使ってチェック制御も可能です。

<Checkbox.Root>
  <Checkbox.Indicator>何らかのチェックマークsvgを置く</Checkbox.Indicator>
  // ここにテキスト
</Checkbox.Root>

最後はコンボボックスです。これはタグが多くややここしいので最低限の構成で今回は実装しています。
構成は以下となります。

<Combobox.Root>
  <Combobox.Input />
  <Combobox.Trigger />
  <Combobox.Portal>
    <Combobox.Positioner>
      <Combobox.Popup>
        <Combobox.List>
          <Combobox.Row>
            <Combobox.Item>
              <Combobox.ItemIndicator />
            </Combobox.Item>
          </Combobox.Row>
        </Combobox.List>
      </Combobox.Popup>
    </Combobox.Positioner>
  </Combobox.Portal>
</Combobox.Root>

Combobox.Input / Combobox.Trigger でまずテキスト入力欄を作成します。
Combobox.Portal から 1 段下ずつ Combobox.Positioner / Combobox.Popup と置くことで選択一覧を出す構成です。
そして、Combobox.List の中に Combobox.Row で選択肢を実装して完成!という感じです。

コンボボックスは出来ることが多いため、公式ドキュメントと照らし合わせるのが現状ベターかなと思っています。

実装したサンプルフォーム

パーツ構成を踏まえ、完成したサンプルフォームはこちらです!

サンプルフォーム初期状態

コンボボックス含め機能していることが確認できました。

サンプルフォームのコンボボックスを開いた状態

完成したフォームの全体像。
import { Form } from "@base-ui/react/form";
import { Field } from "@base-ui/react/field";
import { Combobox } from "@base-ui/react/combobox";
import { Checkbox } from "@base-ui/react/checkbox";

const countries = [
  "日本",
  "アメリカ",
  "イギリス",
  "フランス",
  "ドイツ",
  "カナダ",
  "オーストラリア",
];

export function SampleForm() {
  const handleSubmit = async (formValues: Record<string, any>) => {
    console.log("Form submitted:", formValues);
  };

  return (
    <div className="max-w-md mx-auto p-6 rounded-lg border border-gray-300 bg-white">
      <h2 className="text-2xl font-bold mb-6">サンプルフォーム</h2>

      <Form onFormSubmit={handleSubmit} className="space-y-4">
        {/* Text Input Fields */}
        <Field.Root name="name">
          <Field.Label className="block text-sm font-medium text-gray-700 mb-2">
            お名前
          </Field.Label>
          <Field.Control
            className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            type="text"
            placeholder="山田 太郎"
            required
          />
          <Field.Description className="text-sm text-gray-500 mt-2">
            フルネームを入力してください
          </Field.Description>
        </Field.Root>

        {/* Combobox Field */}
        <Field.Root name="country">
          <Combobox.Root items={countries}>
            <div className="relative">
              <Field.Label className="block text-sm font-medium text-gray-700 mb-2">
                居住国
              </Field.Label>
              <Combobox.Input
                className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                placeholder="国を選択..."
              />
              <Combobox.Trigger className="absolute right-2 h-10 top-1/2 -translate-y-1/2">
                <svg
                  width="16"
                  height="16"
                  viewBox="0 0 16 16"
                  fill="currentColor"
                  className="text-gray-400"
                >
                  <path d="M4.427 6.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 6H4.604a.25.25 0 00-.177.427z" />
                </svg>
              </Combobox.Trigger>
              <Field.Description className="text-sm text-gray-500 mt-2">
                現在お住まいの国を選択してください
              </Field.Description>
            </div>
            <Combobox.Portal>
              <Combobox.Positioner sideOffset={5}>
                <Combobox.Popup className="bg-white border border-gray-300 rounded-md shadow-lg w-(--anchor-width) max-h-60 overflow-auto">
                  <Combobox.List>
                    {countries.map((country) => (
                      <Combobox.Item
                        key={country}
                        value={country}
                        className="px-4 py-2 cursor-pointer hover:bg-blue-50 data-highlighted:bg-blue-100"
                      >
                        {country}
                      </Combobox.Item>
                    ))}
                  </Combobox.List>
                </Combobox.Popup>
              </Combobox.Positioner>
            </Combobox.Portal>
          </Combobox.Root>
        </Field.Root>

        {/* Checkbox Fields */}
        <Field.Root name="terms">
          <Field.Label className="flex items-center space-x-3 cursor-pointer">
            <Checkbox.Root className="w-5 h-5 border-2 border-gray-300 rounded data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600 flex items-center justify-center">
              <Checkbox.Indicator>
                <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
                  <path
                    d="M10 3L4.5 8.5L2 6"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                </svg>
              </Checkbox.Indicator>
            </Checkbox.Root>
            <span className="text-sm font-medium text-gray-700">
              利用規約に同意します
            </span>
          </Field.Label>
          <Field.Description className="ml-8 text-sm text-gray-500 mt-1">
            サービスを利用するには利用規約への同意が必要です
          </Field.Description>
        </Field.Root>

        <Field.Root name="newsletter">
          <Field.Label className="flex items-center space-x-3 cursor-pointer">
            <Checkbox.Root className="w-5 h-5 border-2 border-gray-300 rounded data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600 flex items-center justify-center">
              <Checkbox.Indicator>
                <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
                  <path
                    d="M10 3L4.5 8.5L2 6"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                </svg>
              </Checkbox.Indicator>
            </Checkbox.Root>
            <span className="text-sm font-medium text-gray-700">
              ニュースレターを購読する
            </span>
          </Field.Label>
          <Field.Description className="ml-8 text-sm text-gray-500 mt-1">
            最新情報やお得な情報をメールでお届けします
          </Field.Description>
        </Field.Root>

        {/* Submit Button */}
        <div className="pt-4">
          <button
            type="submit"
            className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed"
          >
            送信
          </button>
        </div>
      </Form>
    </div>
  );
}

今後への課題・期待

メンテナンス頻度

現時点(2025/12)では日々活発な更新が行われており、現状心配はなさそうです。

多くのコンポーネントを管理してきた MUI 発なこともあり、AutoComplete/Combobox コンポーネントに続いて、他ライブラリだと実装コストの問題で追加が見送られてきたコンポーネントの追加にも期待がかかります。

マイルストーンプロジェクト管理も公開されているため、気になる方はチェックしてみてもいいかもしれません。

shadcn/ui への対応

これまで Radix Primitives をベースで提供されてきた shadcn/ui の方針が気になっている方も多いかもしれません。その流れで気になる動きが出てきました。

Base UI の v1 リリース直後に shadcn/create というサイトが急遽発表されました。カスタマイズした shadcn/ui を作れるサイトなのですが、コンポーネントライブラリを Radix / BaseUI で選択できるようになっており海外コミュニティで話題になっていました。

https://x.com/shadcn/status/1999530406744293593

まだ上記サイトの対応にとどまっているものの、これまで使用されてきたコマンド生成側の shadcn/ui での対応も気になるところです。

(2026/01/25 追記)

従来のコマンド生成側も対応しました!既存で提供されてきたコンポーネントをコードそのまま利用できる形となっています。

https://ui.shadcn.com/docs/changelog/2026-01-base-ui

ただし筆者の手元でコンポーネント追加を確認したところ、Base UI か既存の Radix UI をベースにするかの設定は今のところドキュメントでも説明がない状態で、色々パターンを確認しました。

  • すでに Base UI が dependencies に存在する -> Base UI が使用される
  • すでに Radix UI が dependencies に存在する -> Radix UI が使用される
  • どちらも存在しない -> Radix UI が使用される
  • どちらも存在する -> dependencies の数・サイズが多い方で判定?

どうやら上記の設定で動作しているようです[5]

まだ対応してすぐなこともあり、このあたりの説明や UI ライブラリのマイグレーションの検討はもう少し待ったほうが良い印象を受けました。

最後に

これまで業務で Radix Primitives を使ってきたのですが、AutoComplete/Combobox を始めライブラリ内コンポーネントの組み合わせで対処が難しいケースにしばしば遭遇してきました。Base UI はその悩みを少しでも和らげてくれるのではと期待しています。アイコンの表示方法や命名的にパッと見では使い勝手が分かりづらいパーツもあり、まだ思うところは少しあるものプライベートで色々試したら業務でも少しずつ導入していこうと考えています。

記事を読んでいただきありがとうございました。

脚注
  1. 過去の名称である、Popper はご存知の方は多いかもしれません。 ↩︎

  2. この頃の議論についてもう少し知りたい方は、shadcn 氏のポストあたりを追ってみるといいかと思います。 ↩︎

  3. この Liquid Glass レイアウトによる問題はこちらの記事が詳しいです。 ↩︎

  4. ここで使用されている isolation: isolate; について詳しくはMDN 記事を参照してください。 ↩︎

  5. 一度 BaseUI のみ入ったプロジェクトで badge コンポーネントを導入したところ、Radix UI の方で追加されてしまう現象に遭遇しました。 ↩︎

GitHubで編集を提案

Discussion