Headless UIは、Tailwind CSSの開発元であるTailwind Labsが公開している、見た目(CSS)を一切持たないUIコンポーネント集です。ドロップダウンやモーダルの開閉、キーボード操作、ARIA属性の付与だけを受け持ち、デザインはTailwindのクラスで自分で当てます。React版は @headlessui/react 2.2.10(2026年4月7日公開)、Vue版は @headlessui/vue 1.7.23(2024年9月9日公開)が最新で、同じ名前でも使える部品の数が違います。本記事では、両版の差、React版での実装例、Tailwind v4での状態スタイルの書き方、他のヘッドレスUIライブラリとの選び方を、2026年9月28日時点のnpmレジストリと、ローカル環境(Headless UI 2.2.10・React 19.3.0・TypeScript 7.0.2・jsdom、Vue版は1.7.23とVue 3.5.43のSSR、Tailwind CSS 4.3.3)で描画・ビルドした結果をもとに解説します。
まとめ:Headless UIの要点
- 正体:Tailwind Labs製の「スタイルなし・振る舞いとアクセシビリティ入り」のUI部品集。MIT License。
- React版とVue版の機能差:React版は2.2系でフォーム部品やアンカー位置指定まで揃うが、Vue版は1.7系のままで部品が10種類に留まる。
- スタイルの当て方:React版は
data-openやdata-focusなどのdata属性を出すので、Tailwind v4ならdata-open:と書くだけで状態別のスタイルが付く。 - 旧記法:
<Menu.Button>のようなドット記法は型定義で非推奨。新規コードは<MenuButton>を使う。 - 選び方:React+Tailwindで標準的な部品が揃えば足りる構成に向く。Tooltip・Slider・Toastが要る、Vueで新機能を使いたい、といった場合はRadix UI・Base UI・React Aria・Reka UIを検討する。
Headless UIの仕組み:振る舞いとARIAだけを渡す部品
公式サイトの説明は「Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.」です。安定版の1.0.0は2021年4月14日にReact版とVue版が同時に公開されました。部品はクリックやキー入力に応じて開閉・選択の状態を管理し、その状態をHTML属性として外に出します。見た目は利用側が属性を手がかりに付けます。
一般名詞の「ヘッドレスUI」との違い
「ヘッドレスUI」は、見た目を持たずロジックだけを提供するUI部品という設計の総称としても使われます。Radix UI、Base UI、React Ariaもこの意味ではヘッドレスUIです。一方、固有名詞の「Headless UI」はTailwind Labsの製品名で、npmのパッケージ名は @headlessui/react と @headlessui/vue です。検索結果では両者が混在するため、ライブラリ比較の記事を読むときはどちらの意味かを先に確かめると混乱しません。
実際に出力されるHTMLとARIA属性
React版2.2.10をReact 19.3で描画すると、Switchは <button role="switch" aria-checked="true" data-checked> になり、Tabsは role="tablist" の中に role="tab" のボタンが並び、選択中のタブだけが tabindex="0" を持ちます。ARIAの役割やタブのフォーカス順は部品が管理します。利用側はスタイルと内容に加え、部品に応じて状態・イベント処理・ラベルを設定します。
注意点もあります。Field で囲んだ Input は、サーバー描画(SSR)の段階では <label for> の紐付けしか出ず、aria-labelledby と aria-describedby はブラウザでマウントした後に付きます。また open を指定したDialogでも、SSRのHTMLには中身が出ません。2.2.10では「hydration中は <Portal> を描画しない」修正も入っており、モーダルの中身をSEOやJavaScript無効環境で読ませる用途には向きません。
React版とVue版の違い:使える部品と版の差
2026年9月28日時点で、React版とVue版は別々の版番号で配布されています。React版は2024年5月6日のv2.0以降も更新が続いていますが、Vue版の最新安定版は1.7.23です。公式サイトもReact版をトップに置き、Vue版の文書は /v1/vue の下にv1.7として残しています。
| 項目 | @headlessui/react | @headlessui/vue |
|---|---|---|
| 最新版 | 2.2.10(2026-04-07) | 1.7.23(2024-09-09) |
| 対応ランタイム | React 18 / 19 | Vue 3.2以降 |
| 公開している名前の数 | 65 | 48 |
| Checkbox・Input・Select・Textarea | あり | なし |
| Field・Fieldset・Label・Legend | あり | なし |
| anchor(位置の自動調整) | あり | なし |
| data-open 等の状態属性 | あり | data-headlessui-state のみ |
| 週間ダウンロード(9/20〜26) | 約793万 | 約210万 |
公開している名前の数は、ローカルにインストールしたパッケージの Object.keys() で数えた値です。Vue版に無いのはv2.0で追加されたフォーム系(Button・Checkbox・Input・Select・Textarea・Field・Fieldset・Label・Legend・Description)、CloseButton、MenuSection などです。
Vue版の差はスタイルの当て方にも出ます。Vue版のSwitchをSSRで描画すると data-headlessui-state="checked" は付きますが、React版にある data-checked 属性はありません。Vueではスロットの checked を使ってクラスを切り替えるか、後述のプラグインを使います。
<script setup>
import { ref } from 'vue'
import { Switch } from '@headlessui/vue'
const enabled = ref(false)
</script>
<template>
<Switch v-model="enabled" :class="enabled ? 'bg-sky-600' : 'bg-gray-300'"
class="relative inline-flex h-6 w-11 items-center rounded-full">
<span class="sr-only">通知を受け取る</span>
<span :class="enabled ? 'translate-x-6' : 'translate-x-1'"
class="inline-block h-4 w-4 rounded-full bg-white transition" />
</Switch>
</template>
Vueでフォーム部品や位置調整まで含めたヘッドレスUIが必要なら、npmで「Vue port for Radix UI Primitives」と説明されている reka-ui(2026年9月21日に2.10.5)が選択肢に入ります。Nuxtでの構成はNuxtとは:Vue.jsとの違い・レンダリング方式・4系への移行手順と採用判断を実装視点で解説で扱っています。
React版の導入手順と基本コンポーネントの実装例
インストールは1コマンドです。peerDependenciesは react と react-dom の ^18 || ^19 で、依存としてFloating UI、React Ariaのfocus・interactionsフック、TanStack Virtualを内部で使います。Tailwind CSSは必須ではありませんが、以下のReact例はTypeScript(.tsx)で、Tailwind v4を設定済みのプロジェクトを前提にしています。
npm install @headlessui/react
Menu:ドロップダウンの位置指定と表示アニメーション
anchor を渡すと、メニューがボタンの位置に合わせて配置され、画面端では向きが入れ替わります。transition を付けると、開閉の途中で data-closed・data-enter・data-leave 属性が付くので、Tailwindのクラスだけでアニメーションを書けます(v2.1で追加)。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
export default function AccountMenu({ onLogout }: { onLogout: () => void }) {
return (
<Menu>
<MenuButton className="rounded-md bg-gray-800 px-3 py-1.5 text-white data-hover:bg-gray-700 data-open:bg-gray-700">
アカウント
</MenuButton>
<MenuItems
anchor={{ to: 'bottom end', gap: 4 }}
transition
className="w-48 rounded-md bg-white p-1 shadow-lg transition duration-150 ease-out data-closed:scale-95 data-closed:opacity-0"
>
<MenuItem>
<a href="/settings" className="block rounded px-3 py-1.5 data-focus:bg-blue-100">設定</a>
</MenuItem>
<MenuItem disabled>
<span className="block px-3 py-1.5 data-disabled:opacity-50">請求(準備中)</span>
</MenuItem>
<MenuItem>
<button onClick={onLogout} className="block w-full rounded px-3 py-1.5 text-left data-focus:bg-blue-100">ログアウト</button>
</MenuItem>
</MenuItems>
</Menu>
)
}
このコードはTypeScript 7.0.2の型チェックを通り、jsdom上でボタンに下矢印キーを送ると role="menu" の要素が data-anchor="bottom end" 付きで開き、先頭の「設定」に data-focus が付き、無効にした「請求(準備中)」には data-disabled が付きました。公式ドキュメントのキーボード操作表では、ボタン上のEnter・Space・上下矢印で開き、Escで閉じ、英字キーを打つと一致する項目へ移動します。anchor を指定したメニューは <body> 直下へポータル描画されるため、親要素の overflow: hidden で切れません。その代わり、親のCSSセレクタでメニューの中身にスタイルを当てることはできなくなります。
Dialog:モーダルの開閉とフォーカス管理
import { useState } from 'react'
import { Description, Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'
export default function DeleteDialog({ onDelete }: { onDelete: () => void }) {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>削除</button>
<Dialog open={open} onClose={() => setOpen(false)} className="relative z-50">
<DialogBackdrop transition className="fixed inset-0 bg-black/40 duration-200 data-closed:opacity-0" />
<div className="fixed inset-0 flex items-center justify-center p-4">
<DialogPanel transition className="max-w-md rounded-lg bg-white p-6 duration-200 data-closed:scale-95 data-closed:opacity-0">
<DialogTitle className="font-bold">記事を削除しますか</DialogTitle>
<Description>この操作は取り消せません。</Description>
<div className="mt-4 flex gap-2">
<button onClick={() => setOpen(false)}>キャンセル</button>
<button onClick={() => { onDelete(); setOpen(false) }}>削除する</button>
</div>
</DialogPanel>
</div>
</Dialog>
</>
)
}
開いた状態を検査すると、role="dialog" に aria-modal="true" が付き、aria-labelledby がDialogTitle、aria-describedby がDescriptionを指していました。背後のアプリ本体には aria-hidden="true" が付き、<html> に overflow: hidden が入ってスクロールが止まります。フォーカスは先頭のボタンではなくダイアログ要素そのものに移るので、開いた直後のフォーカス先を指定する場合、Headless UIのフォーム部品には autoFocus、通常のHTML要素には data-autofocus を付けます。DialogBackdrop と、兄弟関係にある複数のDialogを同時に開ける機能はv2.1で追加されました。
Field・Label・Description:フォーム部品の自動紐付け
import { Description, Field, Input, Label } from '@headlessui/react'
export default function EmailField({ hasError }: { hasError: boolean }) {
return (
<Field>
<Label>メールアドレス</Label>
<Description>請求書の送付先に使います。</Description>
<Input type="email" name="email" invalid={hasError}
className="rounded border px-2 py-1 data-focus:outline-2 data-invalid:border-red-500" />
</Field>
)
}
hasError を true にして描画すると、inputに aria-invalid="true" と data-invalid、ラベルと説明文へのID参照が自動で付きました。IDを手で採番して htmlFor や aria-describedby を合わせる作業が不要になるのが、v2.0でフォーム部品が加わった一番の効用です。
Tailwind CSSでの状態スタイル:v3・v4の書き方
React版2.x は、開閉や選択の状態を個別のdata属性として出します。Tailwind v4は値を持たないdata属性に対して data-open: のような短縮形を生成するため、Tailwind 4.3.3でビルドして確認したところ、data-closed:opacity-0 は [data-closed] セレクタのCSSになりました。Tailwind v3では同じことを data-[closed]:opacity-0 と角括弧で書きます。
| 状態 | React版の属性 | Tailwind v4 | Tailwind v3 | Vue版(プラグイン使用) |
|---|---|---|---|---|
| 開いている | data-open | data-open: | data-[open]: | ui-open: |
| チェック状態・SwitchのON | data-checked | data-checked: | data-[checked]: | ui-checked: |
| 選択中のタブ | data-selected | data-selected: | data-[selected]: | ui-selected: |
| キーボード等で注目 | data-focus | data-focus: | data-[focus]: | ui-active: |
| 無効 | data-disabled | data-disabled: | data-[disabled]: | ui-disabled: |
| 閉じる前後(遷移) | data-closed | data-closed: | data-[closed]: | なし |
ui-open: などは @headlessui/tailwindcss(0.2.2、peerは tailwindcss ^3.0 || ^4.0)が追加するバリアントで、data-headlessui-state 属性の値を見ます。Tailwind v4では設定ファイルでなくCSSに @plugin "@headlessui/tailwindcss"; と書けば動き、実際に ui-open:rotate-180 が [data-headlessui-state~="open"] のセレクタで出力されました。React版ならdata属性だけで足りるので、このプラグインは主にVue版のためのものと考えてよいでしょう。v3からv4への移行で変わる点はTailwind CSS v3とv4の違い|破壊的変更と移行手順をv4.3.3の実ビルドで検証にまとめています。
旧記法からの移行:Menu.Buttonとactiveの扱い
検索で見つかる解説の多くは、v1時代の <Menu.Button> や <Menu.Items> というドット記法で書かれています。2.2.10の型定義では、これらに「<MenuButton> instead of <Menu.Button>」という @deprecated 注記が付いています。v2.0のリリースノートは、ドット記法をやめた理由をReact Server Componentsとの互換性とツリーシェイキングの改善と説明しています。旧記法のコードもTypeScriptのエラーにはならず動きますが、エディタでは取り消し線が表示されます。
Menu.Button・Menu.Items・Menu.Item→MenuButton・MenuItems・MenuItemDialog.Panel・Dialog.Title→DialogPanel・DialogTitleDialog.Description・DialogDescription→ 共通のDescriptionPopoverOverlay→PopoverBackdropRadioGroup.Option→Radio- MenuItemのレンダープロップ
active→focus(クラスで書くならdata-focus:) Transition.Root・Transition.Child→Transition・TransitionChild(開閉アニメーションだけなら各部品のtransitionプロップで足りる)
開発・保守の状況:リリース間隔とダウンロード数
React版の主な節目は、v2.0(2024年5月6日:アンカー位置指定・フォーム部品・Comboboxの仮想スクロール)、v2.1(2024年6月21日:transitionプロップ・複数Dialog)、v2.2(2024年10月25日:React 19対応)です。その後は2025年4月〜9月に2.2.1〜2.2.9、2026年4月7日に2.2.10が出ましたが、中身はPortalのhydration対策や Fragment のエラー修正といった不具合修正です。
GitHubのtailwindlabs/headlessuiは2026年9月28日の確認ではスター約2.88万、未解決のIssueとPull Requestが合わせて111件です。最後のコミットは2026年4月13日(CIの設定更新)で、それから5か月以上コミットがありません。アーカイブはされておらず開発終了の告知もありませんが、新しい部品が増える前提で採用するのは避けるべきです。
npmのDownloads APIで取得した2026年9月20日〜26日の週間ダウンロード数は、@headlessui/react が約793万、@base-ui/react が約1,650万、react-aria-components が約522万でした。Radix UIは部品ごとにパッケージが分かれており、@radix-ui/react-dialog 単体で約8,409万あります。1年前の同じ週(2025年9月20日〜26日)の @headlessui/react は約320万で、ダウンロード数は約2.5倍になりました。同じ週のBase UIは旧パッケージ名 @base-ui-components/react で約18万でした。ダウンロード数にはCIの自動ビルドなども含まれるため、この比は利用者数や新規採用の件数を示すものではありませんが、両者とも配布量が伸びていることは読み取れます。
Radix UI・Base UI・React Ariaとの選び方
公式サイトの部品一覧はHeadless UIが16種類(Menu・Disclosure・Dialog・Popover・Tabs・Transitionと、Button・Checkbox・Combobox・Fieldset・Input・Listbox・Radio Group・Select・Switch・Textarea)で、Tooltip・Slider・Toast・日付選択は含まれません。
| ライブラリ | 開発元 | 対応 | 向く構成 |
|---|---|---|---|
| Headless UI | Tailwind Labs | React(Vueは1.7系) | Tailwindで標準部品だけ |
| Radix UI | WorkOS | React | Tooltip等も含め幅広く |
| Base UI | MUI | React | shadcn/uiの既定・新規採用 |
| React Aria | Adobe | React | 厳密なアクセシビリティ |
| Reka UI | unovue(有志) | Vue | Vueで部品を揃えたい |
Headless UIを選ぶ構成
React+Tailwind CSSで、必要な部品がメニュー・モーダル・タブ・コンボボックス・フォーム部品の範囲に収まる管理画面や社内ツールなら、Headless UIは記述量が少なく済みます。必要な部品に範囲を絞って学習でき、状態別のスタイルは各部品のdata属性とTailwindのバリアントを対応させて記述できます。Tailwind Labsの有料テンプレートTailwind PlusのReact版は操作部分をHeadless UIに依存しており、UIキットのCatalystも公式ブログで「Powered by the next generation of Headless UI」と紹介されています。これらを使うなら、Headless UIを選ぶのが自然です。
Headless UIを避けるべき構成
次のどれかに当てはまるなら、最初から別のライブラリを選んだ方が後の作り直しが減ります。
- Tooltip・Slider・Toastが必要:Headless UIには無い。Radix UIとは?PrimitivesとThemesの違い・導入手順【radix-ui 1.6対応】かBase UI(React)とは?Radix UI・MUIとの違いと使い方【v1.8対応】のどちらかなら3つとも含まれるので、部品の供給元を1つにまとめられる。日付選択はRadix Primitivesにも無いため、別のライブラリを探す。
- Vueで新規開発する:Vue版の安定版1.7.23にはReact v2相当のフォーム部品やanchorがなく、安定版の更新も止まっている。Reka UIを比較対象に入れる。
- shadcn/uiを使う:shadcn/uiは2026年7月にBase UIを既定にしており、Headless UIとは部品の土台が違う。詳しくはshadcn/uiとは?読み方・導入手順とBase UI既定化の変更点をReact開発者向けに解説。
- 国際化や支援技術への対応を厳密に詰めたい:React Ariaとは?Adobe製アクセシビリティライブラリの構成・使い方・スタイリングを解説のフック群を直接使う方が細かく制御できる。
見た目まで揃った部品が欲しいなら、ヘッドレスではなくMaterial UIとは:v9系の導入からGrid・テーマ設定までコード付きで解説やChakra UIとは?v3の使い方・インストール手順とv2移行の注意点【2026年9月】の方が早く画面を作れます。なお、Headless UIが付けるのはARIA属性とキーボード操作までで、色のコントラストやフォーカスリングの見た目は利用側の責任です。ARIA属性の役割はWAI-ARIAとは何か?その概要と目的をわかりやすく解説で確認できます。
よくある質問
Headless UIとは何ですか?
Tailwind Labsが開発する、CSSを持たないUIコンポーネント集です。開閉・選択・キーボード操作・ARIA属性の付与を受け持ち、見た目はTailwind CSSなどで利用側が付けます。
@headlessui/reactと@headlessui/vueは何が違いますか?
React版は2.2.10(2026年4月)、Vue版は1.7.23(2024年9月)が最新で、Vue版にはCheckbox・Input・Fieldなどのフォーム部品、anchorによる位置調整、data-open 等の状態属性がありません。
Headless UIはTailwind CSSなしでも使えますか?
使えます。依存にTailwind CSSは含まれず、出力されるdata属性や data-headlessui-state 属性を普通のCSSのセレクタで指定すればスタイルを当てられます。
Menu.Buttonのような書き方は今も使えますか?
2.2.10でも動作し型エラーにもなりませんが、型定義で非推奨になっています。新しいコードでは MenuButton・MenuItems・MenuItem と個別にimportして使います。
Headless UIとRadix UIはどちらを選ぶべきですか?
Tailwind CSSを使い、必要な部品がHeadless UIの16種類に収まるならHeadless UIで足ります。TooltipやSliderなども使う、shadcn/uiと組み合わせる、といった場合はRadix UIかBase UIを選びます。