ヘッドレスコンポーネントシステム · 新しいリソース

Headless UIデザインシステム.

Headless UIは、ダイアログ、リストボックス、スイッチ向けに、スタイルを適用しないReactのインタラクションコンポーネントを提供します。このガイドでは、書類共有のフローを例に、標準的なCSSと組み合わせて、ネイティブのフォーカス、選択、ポップアップの動作を実現する方法を紹介します。

OpenDesign ガイド9 章リファレンス項目27件オリジナルアプリケーションガイド
01

Headless UIデザインシステムのインターフェースプレビュー

共有ダイアログを開き、メールアドレスを検証し、キーボードで権限を選択して、ローカルの招待を追加し、公開リンクの状態を切り替えます。

ネイティブのヘッドレスプリミティブを使ったドキュメント共有完全な例を開く
ネイティブの @headlessui/react 2.2.10 と React 19 を使用。色、文字、間隔、レイアウトはすべて独自の例CSSであり、公式のHeadless UIトークンではありません。・ 実装リファレンス
02

Headless UIデザインシステムのデザインリファレンス

Headless UIには公式のビジュアルトークンスケールがありません。以下の数値や色はすべて、このOpenDesignの例にのみ適用されます。リンク先のドキュメントには、動作と状態のAPIが記載されています。テスト済みパッケージ:@headlessui/react 2.2.10。

独自のCSS色の役割

ソース
名前 / ルール値 / 設定用途
--canvas#f5f5f4 ドキュメントの作業領域。
--surface#ffffff 背景、アクセスパネル、ダイアログ。
--ink#1c1917 主要テキスト。
--muted#57534e 補足テキスト。
--line#d6d3d1 パネルと入力欄の境界線。
--accent#4338ca 主要アクションとフォーカス枠。
--accent-soft#eef2ff フォーカス中のオプションとアバターの背景。
--error#b91c1c 無効なメールアドレスのテキストと境界線。

独自の間隔と形状

ソース
名前 / ルール値 / 設定用途
--space-1 / --space-24px / 8px小さな間隔。
--space-3 / --space-412px / 16pxコントロール間の間隔とモバイル時の余白。
--space-6 / --space-824px / 32pxパネル内の余白と大きな区切り。
--radius-control8pxボタンと入力欄。
--radius-panel16px背景とダイアログ。
.dialog-panel widthmin(100%, 460px)狭い画面に収まるオーバーレイ。
.dialog-panel max-heightcalc(100% - 16px)固定配置コンテナ内の余白を確保します。

独自の文字設定とレスポンシブ設計

ソース
名前 / ルール値 / 設定用途
Body16px / 1.5 system-ui外部フォントは不要です。
Screen title28px / 1.25ドキュメントの識別情報。
Document headline32px; 26px below 700px編集コンテンツの階層。
Dialog heading20px / 1.3共有タスクを示します。
Labels and helper copy14pxフィールドの説明。
Error and access description13px常に表示されるフィードバック。
Breakpoint700pxドキュメントとアクセスのパネルを縦に並べます。

ネイティブなインタラクションの仕様

ソース
名前 / ルール値 / 設定用途
Dialog open / onCloseControlled boolean / dismissal callbackEscapeキーや外側の領域での操作は閉じるリクエストを発行しますが、保存はしません。
DialogPanelContent boundary背景と配置用のラッパーは、この境界の外側にあります。
Listbox value / onChangeControlled permission stringキーボードでの選択により、選択中の権限が更新されます。
data-focus / data-selectedNative state attributesCSSは、キーボードフォーカスと確定済みの選択を区別します。
Switch checked / onChangeControlled booleandata-checkedで表示状態をスタイル設定します。
03

Headless UI デザインシステム完全ガイド

実際のインターフェースで、このデザインシステムを選び、適用し、レビューするための実践ガイドです。

DESIGN-headless-ui.md

詳しいガイダンス、実装の出発点、ソースへのリンクをまとめています。

ガイドをダウンロード

Headless UI デザインシステム:画面をスタイリングする前に動作を組み立てる

画面をスタイリングする前に動作を組み立てる

Dialog、Listbox、Switch は、それぞれ異なる操作をモデル化します。Dialog は範囲が限定されたタスクとそのフォーカスコンテキストを管理し、Listbox は確定した単一の選択肢を表し、Switch は独立した真偽値の設定を表します。これらを組み合わせても、状態をひとまとめにする必要はありません。招待の下書き、選択した権限、公開リンクの設定は、それぞれ独立した状態として維持されます。

この OpenDesign ガイドから

この例では React コンポーネント API を使用し、DialogPanel や ListboxButton などを直接インポートします。ダイアログ内にリストボックスがあるため、既存のオーバーレイ内でもポップアップの配置と閉じる操作が正しく機能する必要があります。まずこの組み合わせを実装し、その後にドキュメントパネルとアクセス概要を追加します。付属の CSS は、これらのネイティブパーツの外観を完成させるものです。

Headless UI デザインシステム:Dialog、Listbox、Switch の構造

オーバーレイの境界を意図的に設計する

DialogBackdrop は、固定配置のコンテナと兄弟関係にあります。DialogPanel はそのコンテナ内にある、範囲の限定された白いパネルであり、ビューポート全体を覆うものではありません。DialogTitle はタスク名を示し、Description は閉じたときの動作を説明します。ネイティブの入力欄には data-autofocus が設定され、Headless UI がダイアログ周辺のフォーカス動作を管理します。

関連する仕様を確認する

リストボックスは値を選択し、スイッチは真偽値を切り替える

ListboxButton は選択中のロールを表示します。ListboxOptions では anchor="bottom" と --button-width に基づく幅を指定し、ポップアップをトリガーに揃えます。各 ListboxOption には安定した文字列の値を設定します。ネイティブの data-focus と data-selected 属性で、異なる2種類の CSS スタイルを適用します。

公開リンクの Switch は、ラベルのない装飾的なピルではなく、Label を備えた Field 内に配置されています。チェック状態が変わると、アクセシブルなコントロールと、表示される説明文の両方が変化します。個別の招待は、公開リンクの設定とは独立しています。

関連する仕様を確認する

Headless UI デザインシステム:選択、閉じる操作、ローカルでの確認を分離する

ここで使用する状態モデル

ダイアログを開くと、新しい宛先の下書きと「閲覧可能」ロールが初期設定されます。キャンセル、Escape、または外側のクリックでダイアログを閉じても、宛先は追加されません。送信時にはメールアドレスを検証し、すでに登録されているアドレスであれば拒否したうえで、招待をメモリに追加してダイアログを閉じます。確認メッセージでは、メールを送信していないことを明示します。

権限用のリストボックスが変更するのは下書きのロールであり、既存の宛先のアクセス権ではありません。公開リンクの状態は、別の真偽値として管理されます。こうすることで、閉じる操作をすべて権限の保存として扱うという、よくある誤りを避けられます。

  • 入力が無効な場合:ダイアログを開いたままにし、エラーをメール入力欄に関連付けます。
  • 宛先が重複している場合:既存のアクセス一覧は変更しません。
  • ローカルでの招待を受け付けた場合:一覧を更新し、プレビューのみの結果であることを通知します。

Headless UI デザインシステム:選択のスタイルとキーボードフォーカスの位置を個別に設定する

選択のスタイルとキーボードフォーカスの位置を個別に設定する

ListboxOption は、異なる状態を表す data-focus と data-selected を公開します。フォーカス中のオプションには淡いアクセントを適用し、確定済みの権限にはチェックマークを表示します。これにより、キーボード操作でも既存の選択を見失わずに、ほかの選択肢を順に確認できます。

ポップアップの外でも同じ区別が必要です。エラーの役割は無効な宛先フィールドに属し、Switch のチェック時の外観は公開リンクへのアクセス設定に属します。CSS は、それが表すコンポーネントの状態に適用範囲を限定してください。フォーカス中やチェック済みの要素をすべて同じ色にする広範なセレクターでは、こうした異なる意味が分かりにくくなります。

CSS
:root { --accent: #4338ca; --accent-soft: #eef2ff; --error: #b91c1c; }
.role-option[data-focus] { background: var(--accent-soft); }
.role-option[data-selected] .selected { visibility: visible; }

コードリファレンス

Headless UI デザインシステム:ドキュメントの識別情報と権限を読み取りやすくする

再現可能なタイポグラフィ設定

ドキュメントタイトルは 28px、行の高さは 1.25 です。ダイアログのタイトルは 20px、行の高さは 1.3 です。本文はプラットフォームのシステムフォントを使用し、16px/1.5 に設定します。フィールドラベルと補足テキストは 14px です。これらの正確な値は style.css に記載されているため、スクリーンショットから推測する必要はありません。

アクセス一覧のメールアドレスには overflow-wrap:anywhere を指定し、列幅が押し広げられないようにします。権限は別の行に表示します。ダイアログ内では、招待を追加する時点でドキュメントのタイトル全体と選択済みの権限を確認できます。

Headless UI デザインシステム:オーバーレイの配置とレスポンシブな CSS を完全に実装する

レイアウトの実装に前提条件は不要

付属のスタイルシートには、固定配置、背景の不透明度、z-index、最大幅、縦方向のスクロール、パディングなど、オーバーレイに必要なスタイルがすべて含まれています。700px 未満ではドキュメントパネルを縦に並べ、ダイアログのパディングを小さくします。Tailwind のビルド環境は前提としていません。

配置用のコンテナと DialogPanel は分けてください。コンテナは利用可能なビューポート全体を覆い、端の余白を確保します。パネルは範囲を限定したコンテンツ面を提供します。ダイアログの内容が長い場合はスクロール領域を設け、狭い画面でも宛先フィールド、権限の選択、閉じる操作にアクセスできるようにします。

CSS
.dialog-position { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; padding: 16px; overflow-y: auto; }
.dialog-panel { width: min(100%, 460px); max-height: calc(100% - 16px); overflow-y: auto; }

コードリファレンス

Headless UI デザインシステム:ネイティブの状態とフォーカスの動作を利用する

キーボード操作の確認

ダイアログを開き、最初に宛先へフォーカスが移ることを確認します。Tab キーで権限のトリガーに移動し、矢印キーと Enter キーでロールを選択します。Escape キーを押すと、ネイティブコンポーネントの現在のフォーカスコンテキストに応じて、開いているポップアップまたはダイアログが閉じます。ダイアログを閉じた後、フォーカスが共有トリガーに戻ることを確認します。

メール入力欄のカスタムエラーには aria-invalid と aria-describedby を使用します。オプション内のチェックマークは装飾目的です。選択状態の意味付けは Listbox が担います。公開リンクのコントロールには表示される Label があり、切り替え後も説明文が表示されます。視差効果を減らす設定 CSS では、装飾的なスイッチのつまみのトランジションがなくなります。

関連する仕様を確認する

Headless UI デザインシステム:React API の生成方法を一貫して使用する

React API の生成方法を一貫して使用する

React の完全なエントリーポイントは App をマウントし、そのスタイルシートがオーバーレイの寸法と状態セレクターを提供します。動作を検証済みの 2.2.10 の例では、DialogPanel、DialogBackdrop、ListboxButton、ListboxOptions を直接インポートします。古い複合コンポーネントの記法をこの構成に混在させず、意図的に移行してください。

ListboxOptions は anchor="bottom" とネイティブの --button-width 変数を使い、オプションをトリガーに揃えます。これらは統合に必要な要素であり、任意の装飾ではありません。配置コンテナを削除したり、幅を無関係な固定値に置き換えたりすると、閉じた状態のフィールドが正しく見えていても、入れ子になったポップアップが正常に動作しなくなることがあります。

  1. Reactプロジェクトに @headlessui/react をインストールします。
  2. 例にある App.jsx と style.css を保存し、手順に従って main.jsx を置き換えます。
  3. 開発サーバーを起動し、ダイアログの閉じ方、権限の選択、重複メールへの対処を確認します。

Headless UIデザインシステム:実際の権限設定を確認する

受け入れ条件

有用なレビューは、空のコンポーネント一覧ではなく、ドキュメントと既存の受信者から始めます。「コメント可」で有効な招待を追加してから、同じメールアドレスを再度入力します。元の行が保持され、重複に対して具体的なエラーが表示されることを確認します。公開リンクを有効にし、その説明が独立して表示されることも確認します。

この例で示すのはローカルの UI 状態のみです。受信者の検索機能も、共有バックエンドもありません。実際のサービスに接続するには、保留中・エラーの状態モデルと、権限変更が同時に行われる場合の明確なポリシーが必要です。認可ルールはネイティブのプリミティブでは決まりません。

Headless UIデザインシステムのコンポーネント例

App.jsx — ネイティブな共有 UI の構成

制御された Dialog、権限を設定する Listbox、ラベル付き Switch、ローカル検証をまとめた例です。

jsx
import React, { useState } from 'react';
import { Dialog, DialogBackdrop, DialogPanel, DialogTitle, Description, Listbox, ListboxButton, ListboxOptions, ListboxOption, Switch, Field, Label } from '@headlessui/react';
import './style.css';
const roles = ['Can view', 'Can comment', 'Can edit'];
export default function App() {
 const [open,setOpen]=useState(false),[role,setRole]=useState(roles[0]),[publicLink,setPublicLink]=useState(false),[email,setEmail]=useState(''),[error,setError]=useState(''),[notice,setNotice]=useState('');
 const [people,setPeople]=useState([{email:'[email protected]',role:'Can edit'}]);
 const start=()=>{setEmail('');setError('');setRole(roles[0]);setOpen(true)};
 const submit=e=>{e.preventDefault();if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)){setError('Enter a complete email address.');return;}if(people.some(p=>p.email===email)){setError('This person already appears in the access list.');return;}setPeople([...people,{email,role}]);setOpen(false);setNotice('Invitation added to this local example only. No email was sent.');};
 return <main className='sharing-shell'>
  <header><div><span className='eyebrow'>HEADLESS UI / CUSTOM SURFACE</span><h1>Quarterly brief</h1><p className='muted'>Research · Updated today</p></div><button className='primary' onClick={start}>Share document</button></header>
  <div className='document-grid'><article className='paper'><span className='tag'>WORKING DOCUMENT</span><h2>Make the next decision clearer.</h2><p>Three research priorities for the coming quarter, shared with the people who need to review them.</p><div className='rule'/><h3>01 / Understand the handoff</h3><p>Compare where context is lost between research notes and the next product decision.</p><h3>02 / Review access deliberately</h3><p>Keep editing rights separate from the ability to read a document.</p></article>
  <aside className='access'><h2>People with access</h2><p className='muted'>The list below is local demonstration state.</p>{people.map((p,i)=><div className='person' key={p.email}><span className='avatar'>{i===0?'AK':'+'}</span><div><strong>{p.email}</strong><small>{p.role}</small></div></div>)}<Field className='link-setting'><Label>Public link</Label><Switch checked={publicLink} onChange={setPublicLink} className='switch'><span /></Switch></Field><p className='link-detail'>{publicLink?'Anyone with the example link could view this document. This changes only the local preview.':'Restricted to the named people in this example.'}</p><p role='status' className='notice'>{notice}</p></aside></div>
  <Dialog open={open} onClose={()=>setOpen(false)} className='dialog-root'><DialogBackdrop className='backdrop'/><div className='dialog-position'><DialogPanel className='dialog-panel'><DialogTitle as='h2'>Share quarterly brief</DialogTitle><Description className='muted'>Choose one recipient and a permission. Closing discards this invitation draft.</Description><form noValidate onSubmit={submit}><label className='field-label' htmlFor='recipient'>Recipient email</label><input id='recipient' data-autofocus type='email' value={email} onChange={e=>{setEmail(e.target.value);setError('')}} aria-invalid={!!error} aria-describedby={error?'email-error':undefined} placeholder='[email protected]'/>{error&&<p id='email-error' role='alert' className='error'>{error}</p>}<Listbox value={role} onChange={setRole}><Label className='field-label'>Permission</Label><ListboxButton className='role-button'>{role}<span aria-hidden='true'>⌄</span></ListboxButton><ListboxOptions anchor='bottom' className='role-options'>{roles.map(value=><ListboxOption key={value} value={value} className='role-option'><span>{value}</span><span className='selected' aria-hidden='true'>✓</span></ListboxOption>)}</ListboxOptions></Listbox><p className='muted permission-help'>{role==='Can edit'?'May change document content in this example.':role==='Can comment'?'May add comments without editing content.':'May read the document without changing it.'}</p><div className='dialog-actions'><button type='button' onClick={()=>setOpen(false)}>Cancel</button><button className='primary' type='submit'>Add invitation</button></div></form></DialogPanel></div></Dialog>
  <footer>Native Dialog, Listbox, and Switch behavior. Original CSS below supplies all visual values; Headless UI does not ship this palette.</footer>
 </main>;
}

コードリファレンス

  • メールの送信やリクエストは行われません。
  • Escape キーまたはキャンセルを選ぶと、保存していない招待は破棄されます。

style.css — 独自ビジュアルシステム一式

App.jsx に必要なすべてのビジュアルトークン、状態セレクター、オーバーレイの配置、レスポンシブルールです。これらは例としての選択であり、Headless UIの公式なデフォルトではありません。

CSS
:root{--canvas:#f5f5f4;--surface:#fff;--ink:#1c1917;--muted:#57534e;--line:#d6d3d1;--accent:#4338ca;--accent-soft:#eef2ff;--error:#b91c1c;--space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--space-6:24px;--space-8:32px;--radius-control:8px;--radius-panel:16px;font:16px/1.5 system-ui,sans-serif;color:var(--ink);background:var(--canvas)}*{box-sizing:border-box}body{margin:0}button,input{font:inherit}button{cursor:pointer;border:1px solid var(--line);border-radius:var(--radius-control);padding:10px 16px;background:var(--surface);color:var(--ink);font-weight:600}button:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.primary{background:var(--accent);color:white;border-color:var(--accent)}.sharing-shell{max-width:1120px;margin:auto;padding:32px}.sharing-shell>header{display:flex;align-items:center;justify-content:space-between;gap:24px;margin-bottom:32px}.eyebrow{font-size:11px;letter-spacing:.1em;color:var(--accent);font-weight:700}h1{font-size:28px;line-height:1.25;margin:8px 0}h2{font-size:20px;line-height:1.3;margin:0 0 12px}h3{font-size:16px;margin:24px 0 8px}p{margin:0 0 16px}.muted{color:var(--muted);font-size:14px}.document-grid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(260px,1fr);gap:24px}.paper,.access{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-panel);padding:32px}.paper h2{font-size:32px;max-width:15ch;margin-top:24px}.paper p{max-width:48ch}.tag{font-size:11px;font-weight:600;border:1px solid var(--line);padding:4px 8px;border-radius:4px}.rule{height:1px;background:var(--line);margin:24px 0}.person{display:flex;gap:12px;padding:16px 0;border-bottom:1px solid var(--line)}.person strong{display:block;font-size:14px;overflow-wrap:anywhere}.person small{color:var(--muted)}.avatar{flex:none;width:32px;height:32px;border-radius:50%;background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;font-size:12px}.link-setting{display:flex;align-items:center;justify-content:space-between;margin-top:24px;font-weight:600}.switch{padding:3px;width:44px;height:26px;border:0;border-radius:999px;background:#a8a29e}.switch span{display:block;width:20px;height:20px;border-radius:50%;background:white;transition:transform .15s}.switch[data-checked]{background:var(--accent)}.switch[data-checked] span{transform:translateX(18px)}.link-detail{margin-top:12px;font-size:13px;color:var(--muted)}.notice{font-size:13px;color:var(--accent)}footer{font-size:12px;color:var(--muted);margin-top:24px}.dialog-root{position:fixed;inset:0;z-index:20}.backdrop{position:fixed;inset:0;background:rgb(28 25 23/.45)}.dialog-position{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:16px;overflow-y:auto}.dialog-panel{width:min(100%,460px);max-height:calc(100% - 16px);overflow-y:auto;background:var(--surface);padding:32px;border-radius:var(--radius-panel);box-shadow:0 24px 64px rgb(0 0 0/.22)}.field-label{display:block;font-size:14px;font-weight:600;margin:20px 0 8px}input[type=email]{display:block;width:100%;padding:10px 12px;border:1px solid var(--line);border-radius:var(--radius-control);background:white;color:var(--ink)}input[aria-invalid=true]{border-color:var(--error)}.error{font-size:13px;color:var(--error);margin:8px 0}.role-button{display:flex;justify-content:space-between;width:100%;text-align:left}.role-options{z-index:30;width:var(--button-width);--anchor-gap:4px;background:var(--surface);border:1px solid var(--line);padding:4px;border-radius:8px;box-shadow:0 8px 24px rgb(0 0 0/.12);outline:none}.role-option{display:flex;justify-content:space-between;padding:10px;border-radius:4px;cursor:pointer;font-size:14px}.role-option[data-focus]{background:var(--accent-soft)}.role-option .selected{visibility:hidden;color:var(--accent)}.role-option[data-selected] .selected{visibility:visible}.permission-help{margin-top:12px}.dialog-actions{display:flex;justify-content:flex-end;gap:12px;margin-top:24px}@media(max-width:700px){.sharing-shell{padding:20px}.sharing-shell>header{align-items:flex-start;flex-direction:column}.document-grid{grid-template-columns:1fr}.paper,.access{padding:24px}.paper h2{font-size:26px}.dialog-panel{padding:24px}}@media(prefers-reduced-motion:reduce){.switch span{transition:none}}

コードリファレンス

  • data-focus と data-selected は別々に保ちます。
  • Tailwind は不要です。
04

Headless UIデザインシステムのエージェント設定

実装の出発点

スタイルとコンポーネントのファイル一式は上記にあります。選択した Vite のリリースでサポートされる Node バージョンが必要です。デモでは同じネイティブコンポーネントパッケージをローカルにバンドルしています。

テキスト
# Terminal
npm create vite@latest sharing-demo -- --template react
cd sharing-demo
npm install
npm install @headlessui/[email protected]

# Save the complete examples as src/App.jsx and src/style.css.
# Replace src/main.jsx with:
import React from 'react';
import {createRoot} from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render(<App/>);

# Terminal
npm run dev

あなたのプロジェクト向けプロンプト

AI エージェント用プロンプト
付属の Headless UI App.jsx と style.css を使用します。ネイティブの Dialog、Listbox、Switch の構成を維持し、権限の下書きと確定済みのアクセス権を分け、フォーカス状態と選択状態のすべてのセレクターを残します。ローカルで招待を追加する処理は、保留中と失敗の状態を含む、認可済みの API に置き換えてください。onClose で保存してはいけません。独自の CSSパレットをHeadless UIの公式テーマとして説明しないでください。
05

Headless UIデザインシステムの出典と適用範囲

ネイティブの Headless UI 2.2.10 の動作を使った、オリジナルの OpenDesignビジュアル設定と Reactの例です。CSSの値はアプリケーション独自の選択であることを明記しています。このテーマはHeadless UIでは提供されていません。招待と権限変更はローカルのプレビュー内のメモリにのみ保存されます。