01Microsoft Fluent 2 デザインシステムのインターフェースプレビュー
BriefとPeopleを切り替え、ウェブテーマを変更して、入力エラー、重複、ローカルでの追加成功を試せます。招待は送信されません。
Fluent UI React v9のウェブトークンと、文書化されたField、TabList、Dialogの構造を独立したHTML/CSSで解釈したものです。Fluentのネイティブなコンポーネント実行環境ではありません。·
実装リファレンス 02Microsoft Fluent 2 デザインシステムのデザインリファレンス
Fluent UI React v9のウェブ向けリファレンスです。グローバルな数値は、リンク先のReactトークンソースに基づいています。以下のカラー値は、alias/lightColor.ts内の古いサンプル用16進数コメントではなく、brandWebを使ったライトテーマのエイリアスマッピングを反映しています。ダークカラーとTeamsテーマには、それぞれ別のマッピングがあります。ウェブサイトのタイポグラフィ表では現在Subtitle1が20/26とされていますが、Reactのfonts.tsソースでは20/28となっており、この実装では後者に従っています。参照元の確認日:2026年10月3日。lockfileに記録されたバージョンを必ず確認してください。
| 名前 / ルール | 値 / 設定 | 用途 |
|---|
colorNeutralForeground1 | #242424 | 本文の基本色。 |
colorNeutralForeground2 | #424242 | 補助ラベル。 |
colorNeutralForeground3 | #616161 | メタデータ。最終的なコントラストも確認してください。 |
colorNeutralBackground1 | #ffffff | メインの界面表面。 |
colorNeutralBackground2 | #fafafa | 控えめな補助用背景。 |
colorNeutralBackground3 | #f5f5f5 | ワークスペースのキャンバス。 |
colorNeutralStroke1 | #d1d1d1 | 標準的なコンポーネントの境界線。 |
colorNeutralStroke2 | #e0e0e0 | 目立ちにくい区切り線。 |
colorNeutralStrokeAccessible | #616161 | コントラストを高めたコントロールの境界線。 |
colorNeutralForegroundOnBrand | #ffffff | サポート対象のブランドカラー塗りつぶし上のテキスト。 |
colorStrokeFocus1 / colorStrokeFocus2 | #ffffff / #000000 | 2つのフォーカスカラー。コンポーネントの端でコントラストを保ってください。 |
brandWebから解決されるウェブ用ブランドの役割
ソース | 名前 / ルール | 値 / 設定 | 用途 |
|---|
colorBrandBackground | #0f6cbd · brandWeb[80] | webLightThemeのプライマリアクション。 |
colorBrandBackgroundHover | #115ea3 · brandWeb[70] | そのアクションのホバー状態。 |
colorBrandBackgroundPressed | #0c3b5e · brandWeb[40] | 押下状態。汎用的な選択状態としては使いません。 |
colorBrandBackground2 | #ebf3fc · brandWeb[160] | 控えめなブランドカラーの背景。 |
colorBrandForegroundLink | #115ea3 · brandWeb[70] | ライトウェブテーマのリンク文字色。 |
| 名前 / ルール | 値 / 設定 | 用途 |
|---|
spacingHorizontalNone / spacingVerticalNone | 0 | レイアウトの方向に応じて軸を選びます。共通のスペーシング単位です。 |
spacingHorizontalXXS / spacingVerticalXXS | 2px | レイアウトの方向に応じて軸を選びます。共通のスペーシング単位です。 |
spacingHorizontalXS / spacingVerticalXS | 4px | レイアウトの方向に応じて軸を選びます。共通のスペーシング単位です。 |
spacingHorizontalSNudge / spacingVerticalSNudge | 6px | レイアウトの方向に応じて軸を選びます。アイコンの位置合わせ用の微調整です。 |
spacingHorizontalS / spacingVerticalS | 8px | レイアウトの方向に応じて軸を選びます。共通のスペーシング単位です。 |
spacingHorizontalMNudge / spacingVerticalMNudge | 10px | レイアウトの方向に応じて軸を選びます。アイコンの位置合わせ用の微調整です。 |
spacingHorizontalM / spacingVerticalM | 12px | レイアウトの方向に応じて軸を選びます。共通のスペーシング単位です。 |
spacingHorizontalL / spacingVerticalL | 16px | レイアウトの方向に応じて軸を選びます。共通のスペーシング単位です。 |
spacingHorizontalXL / spacingVerticalXL | 20px | レイアウトの方向に応じて軸を選びます。共通のスペーシング単位です。 |
spacingHorizontalXXL / spacingVerticalXXL | 24px | レイアウトの方向に応じて軸を選びます。共通のスペーシング単位です。 |
spacingHorizontalXXXL / spacingVerticalXXXL | 32px | レイアウトの方向に応じて軸を選びます。共通のスペーシング単位です。 |
React ウェブ用タイポグラフィの組み合わせ
ソース | 名前 / ルール | 値 / 設定 | 用途 |
|---|
Caption2 · fontSizeBase100 | 10px / 14px / 400 | サイズ/行高/ウェイト。lineHeightBase100と組み合わせてください。 |
Caption1 · fontSizeBase200 | 12px / 16px / 400 | サイズ/行高/ウェイト。lineHeightBase200と組み合わせてください。 |
Body1 · fontSizeBase300 | 14px / 20px / 400 | サイズ/行高/ウェイト。lineHeightBase300と組み合わせてください。 |
Body2 / Subtitle2 · fontSizeBase400 | 16px / 22px / 400 / 600 | サイズ/行高/ウェイト。lineHeightBase400と組み合わせてください。 |
Subtitle1 · fontSizeBase500 | 20px / 28px / 600 | サイズ/行高/ウェイト。lineHeightBase500と組み合わせてください。 |
Title3 · fontSizeBase600 | 24px / 32px / 600 | サイズ/行高/ウェイト。lineHeightBase600と組み合わせてください。 |
Title2 · fontSizeHero700 | 28px / 36px / 600 | サイズ/行高/ウェイト。lineHeightHero700と組み合わせてください。 |
Title1 · fontSizeHero800 | 32px / 40px / 600 | サイズ/行高/ウェイト。lineHeightHero800と組み合わせてください。 |
LargeTitle · fontSizeHero900 | 40px / 52px / 600 | サイズ/行高/ウェイト。lineHeightHero900と組み合わせてください。 |
Display · fontSizeHero1000 | 68px / 92px / 600 | サイズ/行高/ウェイト。lineHeightHero1000と組み合わせてください。 |
| 名前 / ルール | 値 / 設定 | 用途 |
|---|
fontFamilyBase | 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif | プロバイダーのデフォルトのウェブ用フォントスタックです。周囲のサイトのフォントをFluentのものとして表記しないでください。 |
fontFamilyMonospace | Consolas, 'Courier New', Courier, monospace | コードと技術識別子。 |
fontWeightRegular / Medium / Semibold / Bold | 400 / 500 / 600 / 700 | 対応するタイポグラフィのスケールと組み合わせて、役割に応じたウェイトを使用してください。 |
| 名前 / ルール | 値 / 設定 | 用途 |
|---|
borderRadiusNone | 0 | まずコンポーネントのデフォルトを使用してください。これは利用可能なトークンであり、すべての界面表面に適用する必要はありません。 |
borderRadiusSmall | 2px | まずコンポーネントのデフォルトを使用してください。これは利用可能なトークンであり、すべての界面表面に適用する必要はありません。 |
borderRadiusMedium | 4px | まずコンポーネントのデフォルトを使用してください。これは利用可能なトークンであり、すべての界面表面に適用する必要はありません。 |
borderRadiusLarge | 6px | まずコンポーネントのデフォルトを使用してください。これは利用可能なトークンであり、すべての界面表面に適用する必要はありません。 |
borderRadiusXLarge | 8px | まずコンポーネントのデフォルトを使用してください。これは利用可能なトークンであり、すべての界面表面に適用する必要はありません。 |
borderRadius2XLarge | 12px | まずコンポーネントのデフォルトを使用してください。これは利用可能なトークンであり、すべての界面表面に適用する必要はありません。 |
borderRadius3XLarge | 16px | まずコンポーネントのデフォルトを使用してください。これは利用可能なトークンであり、すべての界面表面に適用する必要はありません。 |
borderRadius4XLarge | 24px | まずコンポーネントのデフォルトを使用してください。これは利用可能なトークンであり、すべての界面表面に適用する必要はありません。 |
borderRadius5XLarge | 32px | まずコンポーネントのデフォルトを使用してください。これは利用可能なトークンであり、すべての界面表面に適用する必要はありません。 |
borderRadius6XLarge | 40px | まずコンポーネントのデフォルトを使用してください。これは利用可能なトークンであり、すべての界面表面に適用する必要はありません。 |
borderRadiusCircular | 10000px | まずコンポーネントのデフォルトを使用してください。これは利用可能なトークンであり、すべての界面表面に適用する必要はありません。 |
| 名前 / ルール | 値 / 設定 | 用途 |
|---|
durationUltraFast | 50ms | 利用可能な実行時トークンです。この例ではホバーにFastを使用し、動きを抑える設定が有効な場合はトランジションを無効にします。 |
durationFaster | 100ms | 利用可能な実行時トークンです。この例ではホバーにFastを使用し、動きを抑える設定が有効な場合はトランジションを無効にします。 |
durationFast | 150ms | 利用可能な実行時トークンです。この例ではホバーにFastを使用し、動きを抑える設定が有効な場合はトランジションを無効にします。 |
durationNormal | 200ms | 利用可能な実行時トークンです。この例ではホバーにFastを使用し、動きを抑える設定が有効な場合はトランジションを無効にします。 |
durationGentle | 250ms | 利用可能な実行時トークンです。この例ではホバーにFastを使用し、動きを抑える設定が有効な場合はトランジションを無効にします。 |
durationSlow | 300ms | 利用可能な実行時トークンです。この例ではホバーにFastを使用し、動きを抑える設定が有効な場合はトランジションを無効にします。 |
durationSlower | 400ms | 利用可能な実行時トークンです。この例ではホバーにFastを使用し、動きを抑える設定が有効な場合はトランジションを無効にします。 |
durationUltraSlow | 500ms | 利用可能な実行時トークンです。この例ではホバーにFastを使用し、動きを抑える設定が有効な場合はトランジションを無効にします。 |
テーマの変更は意味上の切り替えであり、色の単純な反転ではありません
ソース | 名前 / ルール | 値 / 設定 | 用途 |
|---|
colorNeutralBackground1 | #292929 | ライトテーマの#ffffffに代わる、ダークテーマのメインの界面表面。 |
colorNeutralForeground1 | #ffffff | ライトテーマの#242424に代わる、ダークテーマの本文色。 |
colorNeutralForeground2 | #d6d6d6 | ダークテーマのセカンダリテキスト。 |
colorBrandBackground | brandWeb[70] = #115ea3 | ダークテーマのプライマリ塗りつぶし。ライトテーマではbrandWeb[80]を使用します。 |
colorBrandForegroundLink | brandWeb[100] = #479ef5 | ダークテーマのリンクの役割。プライマリボタンの塗りつぶしとは異なります。 |
03Microsoft Fluent 2 デザインシステム完全ガイド
実際のインターフェースで、このデザインシステムを選び、適用し、レビューするための実践ガイドです。
DESIGN-microsoft-fluent.md詳しいガイダンス、実装の出発点、ソースへのリンクをまとめています。
ガイドをダウンロードMicrosoft Fluent 2 デザインシステムとReact v9の実装
ウェブ向けの実装を明示的に選択してください
Fluent 2はクロスプラットフォームのデザイン言語です。このガイドでは、より範囲を絞り、実用性を重視して、統合されたReact v9ライブラリの@fluentui/react-componentsを選びます。ルートはFluentProviderで、この例ではwebLightThemeまたはwebDarkThemeを使用します。Teamsのテーマは、ブランドに合わせた別の設定です。Teamsでおなじみの紫は、Webのデフォルトの青いランプではありません。Windows、iOS、Androidのネイティブリソースも、互換性のあるReactパッケージではありません。
以下の完成例では、プロジェクト概要とローカルのアクセスリストを表示します。Title3とBody1で階層を作り、TabListとTabで2つのビューを切り替え、DialogにField、Input、Selectを組み合わせてアクセスの判断を示します。このコードは、実在の人物を招待する機能を備えているとはうたっていません。このように分けることで、共有サービスを装うことなく、コンポーネントの動作を確認できます。
関連する仕様を確認する
プロバイダーが実際に提供するもの
FluentProviderは、テーマ変数をそのサブツリーに適用します。tokens.colorNeutralBackground2でスタイルを指定したカスタムサーフェスは選択したテーマに追従しますが、白でハードコードしたサーフェスは追従しません。テーマが必要なコンポーネントより上位にプロバイダーを配置してください。ライブラリはスタイリングランタイムを通じてコンポーネントにスタイルを適用します。この例では、存在しないfluent.cssをインポートする必要はありません。
関連する仕様を確認する
Microsoft Fluent 2デザインシステム:セマンティックカラーと実際のWebブランドランプ
選択したブランドに合わせてエイリアスを解決する
ライトテーマのエイリアスソースでは、colorBrandBackgroundにbrand[80]が割り当てられています。WebテーマではbrandWebが使われ、そのステップ80は#0f6cbdです。そのため、この例では#0f6cbdが解決後のプライマリ塗りつぶし色になります。エイリアスファイルには古い説明用の16進数コメントが含まれています。これらのコメントを現在のWebパレットとして読み取ると、誤った結果になります。値を決めるのはコメントではなく、ソース間の関係です。
概要にはcolorNeutralForeground1、補足ラベルにはcolorNeutralForeground2、周囲のワークスペースにはcolorNeutralBackground3を使います。プライマリアクションには、colorBrandBackgroundとcolorNeutralForegroundOnBrandを組み合わせられます。テーマが変わっても、役割を示す名前は変わりません。人物の権限はテキストで示します。青い境界線やタブのインジケーターが示すのは選択状態であり、権限ではありません。
関連する仕様を確認する
ダークモードではマッピングが変わる
ダークモードでは、メインサーフェスは#292929、プライマリテキストは#ffffffに解決されます。プライマリブランドの塗りつぶしにはステップ70が、リンクの前景色にはステップ100が使われます。そのため、プレビューではページの背景色を反転するだけでなく、トークンの役割をまとめて切り替えています。切り替え後は、アクセスダイアログ、選択中のタブ、控えめに表示されるメタデータを確認してください。これらはすべて、同じテーマの適用範囲に含まれます。
関連する仕様を確認する
Microsoft Fluent 2デザインシステムのタイポグラフィ:テキストの役割をランタイムトークンに対応付ける
サイズと行の高さをセットで使う
Body1は、14pxのサイズと20pxの行の高さを持つ、基本の読みやすいスタイルです。Caption1は12/16、Subtitle2は16/22でセミボールド、Title3は24/32でセミボールドです。これらの組み合わせにより、任意の中間サイズに頼らず、タイトル、ラベル、本文を明確に区別できます。上の表には参考として大きなサイズも掲載していますが、コンパクトなワークスペースに見出しディスプレイサイズは必要ありません。
ReactのトークンソースとデザインWebサイトでは、実用上重要な点に違いがあります。ランタイムソースではfontSizeBase500 / lineHeightBase500が20/28ですが、WebのデザインチャートではSubtitle1が20/26です。この例ではランタイムの組み合わせに従っています。このような違いは、モックアップに合う数値を都合よく組み合わせるのではなく、バージョンと実装を確認する際のポイントとして扱ってください。
関連する仕様を確認する
テキストプリセットとフォントスタック
視覚的なプリセットと合わせて、セマンティックなHTMLも使います。Title3 as="h1"は見出しです。見た目をスタイルで整えるだけでは、ドキュメントのアウトラインは作られません。基本のフォントスタックはSegoe UIから始まり、プラットフォームごとの代替フォントを含みます。スクリーンショットと見た目を合わせるためだけに、プロプライエタリなフォントをバンドルしないでください。このサンプルではドキュメントに記載されたスタックを使用しているため、字形の細部は閲覧端末にインストールされたフォントによって異なります。
カスタムテキストには、フォントファミリー、サイズ、太さ、行の高さをまとめたtypographyStyles.body1を使います。fontSizeBase300だけをコピーするより、この組み合わせを優先してください。アクセスリストでは、折り返し可能な読みやすいメールアドレスを使用します。権限ラベルにはセミボールドの強調を使いますが、ページタイトルより目立たせません。
関連する仕様を確認する
Microsoft Fluent 2デザインシステム:ワークスペースの間隔、形状、密度
Reactのスペーシング名はデザインツールの名称とは異なる
Fluentの一般的なレイアウトガイダンスでは、4単位のリズムに基づくグローバルなサイズ名を使用し、位置合わせの調整には2、6、10なども含まれます。Reactでは、軸ごとのエイリアスが公開されています。spacingHorizontalSは8px、Mは12px、Lは16px、XXLは24pxで、垂直方向にも対応する名称があります。size120をspacingHorizontal120に読み替えないでください。そのReact識別子は存在しません。
このレイアウトでは、パネル内側の余白に24px、フォームフィールド間に16px、アクセス行の間隔に12pxを使用しています。ページの正確な幅や2カラムの配置は、アプリケーション側で決めるものであり、Fluentの公式ブレークポイントではありません。画面幅が狭い場合は、概要の下にチームの補足情報を配置し、2つのタブラベルは表示したままにします。
関連する仕様を確認する
形状はコンポーネントのスケールを表す
関連する仕様を確認する
Microsoft Fluent 2デザインシステムのTabList:選択状態、パネル、キーボード操作
選択中の値はアプリケーションの状態で管理する
この例では、アプリケーション側のReact状態でタブを管理し、その値をselectedValueに渡します。onTabSelectはdata.valueを受け取り、その値で状態を更新します。各Tabには固定のvalueがあり、aria-controlsを通じて対応するパネルを参照します。各パネルにはrole="tabpanel"とaria-labelledbyを設定し、条件に応じて非表示にします。コンポーネントがプロジェクト概要やアクセスリストを提供するわけではありません。これらは明示的に描画します。
APIでは、フォーカスと選択を分けています。selectTabOnFocusの既定値はfalseです。単独で動作するサンプルでは、Left/RightとHome/Endでフォーカスを移動し、EnterまたはSpaceでフォーカス中のタブを有効にします。この手動アクティベーションの動作により、予期しないコンテンツの切り替えを起こさずにタブの選択肢を確認できます。タブのラベルは短く保ちましょう。公式のレイアウトガイダンスによると、横並びのタブは自動的に折り返されたり、スクロールしたりしません。
関連する仕様を確認する
カテゴリが増えたら別のパターンを選ぶ
BriefとPeopleは同じワークスペースの密接に関連した2つのビューなので、タブが適しています。組織内のすべてのプロジェクトを一覧にするなら、それはナビゲーションであり、タブをもう1列追加するケースではありません。狭い幅にカテゴリが収まらなくなった場合は、オーバーフローや別の選択パターンを意図的に設計しましょう。ラベルを読めなくなるまで縮めるのは、レスポンシブ対応とは言えません。
関連する仕様を確認する
Microsoft Fluent 2 デザインシステムの実践:アクセシブルなラベルと適切な検証
フィールドは単なる装飾付き入力ではなく、複数要素の組み合わせ
Fieldは、ラベル用スロット、子コントロール、任意のヒント、検証メッセージを組み合わせます。ドキュメントで定義されているコントロールの契約には、id、aria-labelledby、aria-describedby、aria-invalid、aria-requiredが含まれます。Fluentの入力コンポーネントは直接子要素にできます。カスタムコントロールでは、render関数のpropsを利用できます。そのため、メールアドレス用のInputをFieldで囲むほうが、入力欄の上に無関係なテキストを配置するより有用です。
この例では、無効な内容でローカル送信を試みた後に限り、validationStateがerrorになります。アプリケーションはメールアドレス形式の値かどうかと、メモリ内リストとの重複を確認します。入力値は保持され、validationMessageで修正方法を示します。Fieldはその結果を表示して関連付けますが、アドレスが実在するかの確認、招待の送信、アクセス権の付与は行いません。
関連する仕様を確認する
検証状態と権限を区別する
権限を選ぶSelectにはCan viewとCan editがあり、メールアドレス欄には独自のエラー状態があります。アドレスが未完成というだけでダイアログ全体を赤くするのは避けましょう。requiredはアクセシビリティのための表示であり、サーバー側の認可や検証の代わりにはなりません。このサンプルフォームでは、明示的な検証メッセージを一貫して示すためにnoValidateを使用しています。本番サービスでは、リクエストを再度検証する必要があります。
- アドレスが空、または形式が不正な場合:ダイアログを開いたままにし、受け付ける形式を説明します。
- アドレスがローカルのアクセスリストにすでにある場合:重複専用のメッセージを表示し、行を追加しません。
- 新しい有効なアドレスの場合:選択された権限をローカルに追加し、ダイアログを閉じて、ローカルのみで行われた処理であることが伝わる結果を表示します。
関連する仕様を確認する
Microsoft Fluent 2 デザインシステムのDialog:サポートされている構成でフォームを作る
送信ボタンをフォーム内に置く
構成要素は、Dialog、DialogTrigger、DialogSurface、form要素、そしてDialogTitle、DialogContent、DialogActionsを含むDialogBodyです。フォームが本文を囲むことで、フッターの送信ボタンもフォームの送信に参加します。キャンセルボタンはtype="button"にして、ダイアログを閉じるDialogTriggerの中に配置します。そうしないと、ボタンの既定のtypeによって誤ってフォームが送信されることがあります。
openとonOpenChangeで表示状態を制御します。disableButtonEnhancementを指定すると、すでにボタンとしての動作を備えたFluent Buttonがトリガーによって重複して拡張されるのを防げます。成功ハンドラーは、ローカル検証に通った場合にのみダイアログを閉じます。検証に失敗した場合は、同じメールアドレスと権限を修正に使える状態で保持します。
関連する仕様を確認する
実際の判断内容に合ったモーダル動作を選ぶ
この簡潔なアクセス設定では、既定のモーダルタイプを使用します。非モーダルダイアログなら背後の作業を続けられますが、アラートダイアログは緊急の判断を想定しており、閉じ方にもより厳しい制約があります。通常のアクセス設定フォームにアラートのセマンティクスは必要ありません。公式ガイダンスでは、ダイアログ内にフォーカスを移し、閉じた後はトリガーに戻すことが求められています。この単独サンプルでは、ネイティブのdialog要素を使い、トリガーへのフォーカス復帰を明示的に行います。
関連する仕様を確認する
Microsoft Fluent 2 デザインシステムのカスタムスタイルでテーマを損なわない
makeStylesとセマンティック変数を使う
コード全体では、背景層、間隔、角丸にトークンを使うmakeStylesを使用しています。これにより、カスタムパネルをプロバイダーのテーマに合わせられます。生成されたクラス名は実装の詳細なので、DevToolsからコピーしてスタイルシートに貼り付けないでください。テーマを試すときは、プロバイダーのテーマを変更し、ネイティブコントロールとカスタム領域の両方をまとめて確認しましょう。
公開されている時間の段階は50msから500msまでです。HTMLのサンプルでは、単純なホバー時の変化にdurationFast(150ms)を使用し、prefers-reduced-motionの設定時にはトランジションをなくします。これは利用可能なトークンを使ったアプリケーション上の選択であり、Fluentのすべてのインタラクションを150msにするべきだという意味ではありません。動きをなくしても、コンテンツやエラーメッセージは理解できます。
関連する仕様を確認する
解釈の適用範囲を確認する
関連する仕様を確認する
Microsoft Fluent 2 デザインシステムの完全な構成をインストールして検証する
手順の概要ではなく、実行できるエントリーポイント
このセットアップは、idがrootの要素を持つ既存のReactアプリケーションを対象としています。v9のスイートをインストールし、JSXのエントリーポイントをアプリケーションに配置します。インポート、プロバイダー、コンポーネントツリー、ハンドラー、状態管理、ルートのレンダリングをまとめて示しています。外部の共有APIは呼び出しません。スターターがすでにAppをレンダリングしている場合は、ルートのエントリーポイントをそのまま使い、ルートをもう1つ作るのではなくAppをエクスポートしてください。
解決されたパッケージバージョンをロックファイルに記録してください。リンク先のソース資料は、調査時点のReact v9の実装を説明していますが、master URLの内容は変わることがあります。アップグレードする前に、エクスポートされたテーマと、ここで使用しているField、TabList、Dialogの正確な動作を比較してください。このセットアップでは、v8の@fluentui/reactコンポーネントとv9の例を意図的に混在させていません。
- 既存の React プロジェクトに @fluentui/react-components@9 をインストールし、生成されたロックファイルをそのまま保持します。
- 用意されたエントリーを 1 つの FluentProvider で囲み、初期テーマに webLightTheme を設定してレンダリングします。
- ダイアログを開き、無効なアドレスを送信してから修正します。追加したユーザーがローカルの状態にのみ反映されることを確認します。
- webDarkTheme とキーボード操作でも同じ確認を行い、パネルとの関連付け、エラーの通知、フォーカスの復帰を検証します。
関連する仕様を確認する
実際の共有プロダクトで連携すべき内容
ローカルでの追加を置き換える前に、サービスの契約を定義します。認証済みの実行ユーザー、対象のワークスペース、許可される権限値、招待の結果、重複時の動作を明確にしてください。サービスからリクエストを拒否された場合はダイアログを開いたままにし、アクセス権を楽観的に付与したと見せかけず、確定した結果を表示します。こうしたルールは Fluent の範囲外ですが、コードでは汎用的な送信ボタンの裏に隠さず、引き継ぎ箇所を明示しています。
関連する仕様を確認する
Microsoft Fluent 2 デザインシステムのコンポーネント例
完全なスタンドアロンのインタラクティブ見本
ドキュメント全体を index.html として保存し、ブラウザーで開きます。単独で動作する HTML/CSS による、Fluent UI React v9 の Web トークンと、ドキュメントに記載された Field、TabList、Dialog の構造の解釈です。Fluent のネイティブなコンポーネントランタイムではありません。
html
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Fluent web workspace study</title><style>
:root{color-scheme:light;--colorNeutralForeground1:#242424;--colorNeutralForeground2:#424242;--colorNeutralForeground3:#616161;--colorNeutralBackground1:#fff;--colorNeutralBackground2:#fafafa;--colorNeutralBackground3:#f5f5f5;--colorNeutralStroke1:#d1d1d1;--colorNeutralStroke2:#e0e0e0;--colorBrandBackground:#0f6cbd;--colorBrandBackgroundHover:#115ea3;--colorBrandBackgroundPressed:#0c3b5e;--colorBrandBackground2:#ebf3fc;--colorBrandForegroundLink:#115ea3;--colorNeutralForegroundOnBrand:#fff;--borderRadiusMedium:4px;--borderRadiusXLarge:8px;--spacingHorizontalS:8px;--spacingHorizontalM:12px;--spacingHorizontalL:16px;--spacingHorizontalXXL:24px;--durationFast:150ms}
[data-theme=dark]{color-scheme:dark;--colorNeutralForeground1:#fff;--colorNeutralForeground2:#d6d6d6;--colorNeutralForeground3:#adadad;--colorNeutralBackground1:#292929;--colorNeutralBackground2:#1f1f1f;--colorNeutralBackground3:#141414;--colorNeutralStroke1:#666;--colorNeutralStroke2:#525252;--colorBrandBackground:#115ea3;--colorBrandBackgroundHover:#0f6cbd;--colorBrandBackground2:#082338;--colorBrandForegroundLink:#479ef5}
*{box-sizing:border-box}body{margin:0;background:var(--colorNeutralBackground3);color:var(--colorNeutralForeground1);font-family:'Segoe UI','Segoe UI Web (West European)',-apple-system,BlinkMacSystemFont,Roboto,'Helvetica Neue',sans-serif;font-size:14px;line-height:20px}button,input,select{font:inherit}button{cursor:pointer;min-height:32px;padding:5px 12px;border:1px solid var(--colorNeutralStroke1);border-radius:var(--borderRadiusMedium);background:var(--colorNeutralBackground1);color:var(--colorNeutralForeground1);font-weight:600;transition:background var(--durationFast)}button:hover{background:var(--colorNeutralBackground3)}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--colorBrandForegroundLink);outline-offset:3px}.primary{background:var(--colorBrandBackground);color:var(--colorNeutralForegroundOnBrand);border-color:transparent}.primary:hover{background:var(--colorBrandBackgroundHover)}.primary:active{background:var(--colorBrandBackgroundPressed)}.topbar{display:flex;justify-content:space-between;align-items:center;padding:12px 24px;background:var(--colorNeutralBackground1);border-bottom:1px solid var(--colorNeutralStroke2);gap:16px}.brand{display:flex;align-items:center;gap:12px;font-weight:600}.mark{display:grid;grid-template-columns:repeat(2,7px);gap:3px}.mark i{width:7px;height:7px;background:var(--colorBrandBackground)}.top-note{font-size:12px;color:var(--colorNeutralForeground3)}.page{max-width:1100px;margin:auto;padding:24px}.heading{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:16px}.eyebrow{font-size:12px;line-height:16px;color:var(--colorNeutralForeground3);margin-bottom:4px}h1{font-size:24px;line-height:32px;font-weight:600;margin:0 0 4px}h2{font-size:20px;line-height:28px;font-weight:600;margin:0 0 16px}h3{font-size:16px;line-height:22px;font-weight:600;margin:20px 0 8px}p{margin:0 0 12px}.muted{color:var(--colorNeutralForeground3)}.tabs{display:flex;gap:16px;border-bottom:1px solid var(--colorNeutralStroke2);margin-bottom:24px}.tab{position:relative;background:none;border:0;border-radius:0;font-weight:400;padding:12px 4px;color:var(--colorNeutralForeground2)}.tab[aria-selected=true]{font-weight:600;color:var(--colorBrandForegroundLink)}.tab[aria-selected=true]:after{content:'';position:absolute;bottom:0;left:4px;right:4px;height:3px;background:var(--colorBrandBackground);border-radius:3px}.layout{display:grid;grid-template-columns:minmax(0,1fr) 240px;gap:24px}.document,.aside,.people{background:var(--colorNeutralBackground1);border:1px solid var(--colorNeutralStroke2);border-radius:var(--borderRadiusXLarge);padding:24px}.document{max-width:720px}.docmeta{display:flex;gap:8px;align-items:center;color:var(--colorNeutralForeground3);font-size:12px;line-height:16px;margin-bottom:24px}.avatar{flex:none;display:grid;place-items:center;background:var(--colorBrandBackground2);color:var(--colorBrandForegroundLink);font-weight:600;width:32px;height:32px;border-radius:50%}.quote{padding:12px 16px;background:var(--colorNeutralBackground2);border-left:3px solid var(--colorBrandBackground);margin:16px 0}.checklist{list-style:none;padding:0;margin:12px 0}.checklist li{display:flex;gap:8px;margin:8px 0}.checklist input{accent-color:var(--colorBrandBackground);width:16px;height:16px;margin-top:2px}.aside{padding:20px;height:fit-content}.aside h2{font-size:16px;line-height:22px;margin-bottom:16px}.person{display:flex;gap:8px;align-items:center;margin-bottom:16px}.person p{margin:0}.small{font-size:12px;line-height:16px}.aside hr{border:0;border-top:1px solid var(--colorNeutralStroke2);margin:20px 0}.tag{display:inline-block;border:1px solid var(--colorNeutralStroke1);padding:2px 8px;border-radius:4px;font-size:12px;color:var(--colorNeutralForeground2)}.people-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;gap:12px}.people-head h2{margin:0}.people ul{list-style:none;padding:0;margin:0}.member{display:flex;align-items:center;gap:12px;padding:16px 0;border-top:1px solid var(--colorNeutralStroke2)}.member-info{flex:1;min-width:0;overflow-wrap:anywhere}.member-info p{margin:0}.member .tag{flex:none}.caption{margin:20px 0 0;color:var(--colorNeutralForeground3);font-size:12px;line-height:16px}.status{margin-top:16px;overflow-wrap:anywhere;color:var(--colorNeutralForeground2)}.status:empty{display:none}[hidden]{display:none!important}dialog{width:min(520px,calc(100vw - 32px));padding:24px;border:1px solid var(--colorNeutralStroke1);border-radius:8px;background:var(--colorNeutralBackground1);color:var(--colorNeutralForeground1);max-height:calc(100dvh - 32px);overflow:auto}dialog::backdrop{background:rgba(0,0,0,.4)}dialog h2{font-size:20px;line-height:28px;margin-bottom:12px}.field{margin:20px 0}.field label{display:block;font-weight:600;margin-bottom:8px}.required{color:#b10e1e}.field input,.field select{width:100%;min-height:36px;background:var(--colorNeutralBackground1);color:var(--colorNeutralForeground1);padding:7px 12px;border:1px solid var(--colorNeutralStroke1);border-bottom-color:var(--colorNeutralForeground3);border-radius:4px}.field input:focus{border-bottom:2px solid var(--colorBrandBackground);padding-bottom:6px}.hint{font-size:12px;line-height:16px;color:var(--colorNeutralForeground3);margin-top:8px}.error{font-size:12px;line-height:16px;color:#b10e1e;margin-top:8px}.field input[aria-invalid=true]{border-color:#b10e1e}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:24px}[data-theme=dark] .error,[data-theme=dark] .required{color:#ff99a4}footer{padding:0 24px 20px;text-align:center;font-size:12px;color:var(--colorNeutralForeground3)}@media(max-width:700px){.page{padding:16px}.topbar{padding:12px 16px}.top-note{display:none}.layout{grid-template-columns:1fr}.document,.people{padding:20px}.aside{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px}.aside h2,.aside hr{grid-column:1/-1}.aside hr{margin:0}.heading{align-items:flex-start}.heading .primary{flex:none}.member{flex-wrap:wrap}.member .tag{margin-left:44px}.topbar button,.heading button{min-height:40px}}@media(max-width:420px){h1{font-size:20px;line-height:28px}.heading{flex-direction:column;gap:12px}.aside{display:block}.member .tag{margin-left:0}}@media(prefers-reduced-motion:reduce){*{transition:none!important}}
</style></head><body>
<header class="topbar"><div class="brand"><span class="mark" aria-hidden="true"><i></i><i></i><i></i><i></i></span>Northstar workspace <span class="top-note">/ Product team</span></div><button id="theme" aria-pressed="false">Dark theme</button></header>
<main class="page"><div class="heading"><div><div class="eyebrow">Team workspace · October launch</div><h1>Launch workspace</h1><p class="muted">One brief. A clear decision. The right people.</p></div><button class="primary" id="share">Add a person</button></div>
<div class="tabs" role="tablist" aria-label="Workspace views"><button class="tab" id="brief-tab" role="tab" aria-controls="brief-panel" aria-selected="true" tabindex="0">Brief</button><button class="tab" id="people-tab" role="tab" aria-controls="people-panel" aria-selected="false" tabindex="-1">People <span id="count">2</span></button></div>
<section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab" tabindex="0"><div class="layout"><article class="document"><div class="docmeta"><span class="avatar" aria-hidden="true">AC</span><span>Alex Chen · Product design<br>Updated today at 09:30</span></div><h2>A more welcoming first project</h2><p>Help a new team move from an empty workspace to its first useful result. This review focuses on the welcome screen and the first-project setup.</p><h3>The decision</h3><div class="quote">Should a sample project be created automatically, or offered as an option?</div><p>We want a clear starting point without filling the workspace with content someone did not choose. Review both paths before the team walkthrough.</p><h3>Review checklist</h3><ul class="checklist"><li><input id="c1" type="checkbox" checked><label for="c1">Welcome copy explains the next step</label></li><li><input id="c2" type="checkbox"><label for="c2">Empty workspace offers a sample project</label></li><li><input id="c3" type="checkbox"><label for="c3">Keyboard path reaches the first project action</label></li></ul><p class="small muted">Checklist changes are local to this example.</p></article><aside class="aside"><h2>Working together</h2><div class="person"><span class="avatar" aria-hidden="true">AC</span><div><p>Alex Chen</p><span class="small muted">Workspace owner</span></div></div><div class="person"><span class="avatar" aria-hidden="true">MP</span><div><p>Maya Patel</p><span class="small muted">Can edit</span></div></div><hr><div><h3>Next review</h3><p>Thursday, 10:00<br><span class="muted small">Team walkthrough · UTC</span></p></div><div><h3>Access</h3><span class="tag">Specific people</span><p class="small muted" style="margin-top:8px">Adding a person here updates only this local preview.</p></div></aside></div></section>
<section id="people-panel" role="tabpanel" aria-labelledby="people-tab" tabindex="0" hidden><div class="people"><div class="people-head"><h2>People with access</h2><span class="tag">Local example</span></div><p class="muted">The roles below are fictional. No invitation or real access change is made.</p><ul id="members"><li class="member"><span class="avatar" aria-hidden="true">AC</span><div class="member-info"><p>Alex Chen</p><span class="small muted">[email protected]</span></div><span class="tag">Owner</span></li><li class="member"><span class="avatar" aria-hidden="true">MP</span><div class="member-info"><p>Maya Patel</p><span class="small muted">[email protected]</span></div><span class="tag">Can edit</span></li></ul></div></section><p class="status" id="status" role="status"></p><p class="caption">Fluent UI React v9 web-token study · HTML/CSS interpretation, not native library components. Try tabs, theme, checkboxes and the access form.</p></main>
<dialog id="access" aria-labelledby="dialog-title"><form id="access-form" novalidate><h2 id="dialog-title">Add a person to Launch workspace</h2><p class="muted">Choose an address and permission. This demonstration updates the local list only.</p><div class="field"><label for="email">Email address <span class="required" aria-hidden="true">*</span></label><input id="email" type="email" autocomplete="email" aria-required="true" aria-describedby="email-hint" autofocus><div class="hint" id="email-hint">For example, [email protected]</div><div class="error" id="error" role="alert" hidden></div></div><div class="field"><label for="permission">Permission</label><select id="permission" aria-describedby="permission-hint"><option>Can view</option><option>Can edit</option></select><div class="hint" id="permission-hint">Viewers can read; editors can change the brief in a real application</div></div><div class="actions"><button type="button" id="cancel">Cancel</button><button class="primary" type="submit">Add locally</button></div></form></dialog>
<script>
const $=id=>document.getElementById(id),tabs=[...document.querySelectorAll('[role=tab]')],emails=new Set(['[email protected]','[email protected]']);
function selectTab(tab){tabs.forEach(t=>{const chosen=t===tab;t.setAttribute('aria-selected',String(chosen));t.tabIndex=chosen?0:-1;$(t.getAttribute('aria-controls')).hidden=!chosen;});}
tabs.forEach((tab,index)=>{tab.addEventListener('click',()=>selectTab(tab));tab.addEventListener('keydown',event=>{let next;if(event.key==='ArrowRight')next=(index+1)%tabs.length;if(event.key==='ArrowLeft')next=(index+tabs.length-1)%tabs.length;if(event.key==='Home')next=0;if(event.key==='End')next=tabs.length-1;if(next!==undefined){event.preventDefault();tabs.forEach(t=>t.tabIndex=-1);tabs[next].tabIndex=0;tabs[next].focus();}});});
$('theme').onclick=()=>{const dark=document.documentElement.dataset.theme!=='dark';document.documentElement.dataset.theme=dark?'dark':'light';$('theme').textContent=dark?'Light theme':'Dark theme';$('theme').setAttribute('aria-pressed',String(dark));};
const dialog=$('access'),email=$('email'),error=$('error');function clearError(){error.hidden=true;error.textContent='';email.removeAttribute('aria-invalid');email.setAttribute('aria-describedby','email-hint');}function showError(message){error.textContent=message;error.hidden=false;email.setAttribute('aria-invalid','true');email.setAttribute('aria-describedby','email-hint error');email.focus();}
$('share').onclick=()=>{clearError();dialog.showModal();email.focus();};$('cancel').onclick=()=>dialog.close();dialog.addEventListener('close',()=>$('share').focus());dialog.addEventListener('click',event=>{if(event.target===dialog){const r=dialog.getBoundingClientRect();if(event.clientX<r.left||event.clientX>r.right||event.clientY<r.top||event.clientY>r.bottom)dialog.close();}});email.addEventListener('input',clearError);
$('access-form').addEventListener('submit',event=>{event.preventDefault();const value=email.value.trim().toLowerCase();if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)){showError('Enter an email address such as [email protected]');return;}if(emails.has(value)){showError('This address already appears in the local access list');return;}const permission=$('permission').value;emails.add(value);const item=document.createElement('li');item.className='member';const avatar=document.createElement('span');avatar.className='avatar';avatar.setAttribute('aria-hidden','true');avatar.textContent=value.slice(0,2).toUpperCase();const info=document.createElement('div');info.className='member-info';const name=document.createElement('p');name.textContent=value;const sub=document.createElement('span');sub.className='small muted';sub.textContent='Added in this local example';info.append(name,sub);const role=document.createElement('span');role.className='tag';role.textContent=permission;item.append(avatar,info,role);$('members').append(item);$('count').textContent=emails.size;$('status').textContent='Added '+value+' as '+permission.toLowerCase()+' locally. No invitation was sent.';email.value='';clearError();selectTab($('people-tab'));dialog.close();});
</script></body></html>
コードリファレンス
- このドキュメントのアクセスダイアログとタブには、ネイティブの HTML コントロールを使用しています。FluentProvider、Field、Dialog の動作を評価するには、別途 React v9 のエントリーを実行してください。
完全なネイティブ実装のエントリ
セットアップの完全な実装を、焦点を絞った API の解説とあわせてここにまとめています。セットアップに記載されたパッケージとビルドの前提条件に従ってください。
jsx
// Terminal: npm install @fluentui/react-components@9
// Existing React application; index.html must contain <div id="root"></div>.
// Entry file: src/main.jsx
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import {
FluentProvider, webLightTheme, webDarkTheme, makeStyles, tokens,
Button, Body1, Title3, TabList, Tab, Field, Input, Select,
Dialog, DialogTrigger, DialogSurface, DialogBody, DialogTitle,
DialogContent, DialogActions
} from '@fluentui/react-components';
const useStyles = makeStyles({
page: { maxWidth: '880px', margin: '0 auto', padding: tokens.spacingHorizontalXXL },
header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: tokens.spacingHorizontalL },
panel: { padding: tokens.spacingHorizontalXXL, marginTop: tokens.spacingVerticalL,
backgroundColor: tokens.colorNeutralBackground2, borderRadius: tokens.borderRadiusXLarge },
form: { display: 'grid', gap: tokens.spacingVerticalL },
row: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: tokens.spacingHorizontalM,
paddingTop: tokens.spacingVerticalM, paddingBottom: tokens.spacingVerticalM },
});
function Workspace({ dark, setDark }) {
const styles = useStyles();
const [tab, setTab] = React.useState('brief');
const [open, setOpen] = React.useState(false);
const [email, setEmail] = React.useState('');
const [permission, setPermission] = React.useState('Can view');
const [error, setError] = React.useState('');
const [notice, setNotice] = React.useState('');
const [people, setPeople] = React.useState([{ email: '[email protected]', permission: 'Owner' }]);
function submit(event) {
event.preventDefault();
const value = email.trim().toLowerCase();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
setError('Enter an email address such as [email protected]'); return;
}
if (people.some(person => person.email === value)) {
setError('This address already appears in the local access list'); return;
}
setPeople(previous => [...previous, { email: value, permission }]);
setNotice(`Added ${value} as ${permission.toLowerCase()} in this local example. No invitation was sent.`);
setEmail(''); setError(''); setOpen(false); setTab('people');
}
return <main className={styles.page}>
<div className={styles.header}>
<Title3 as="h1">Launch workspace</Title3>
<Button onClick={() => setDark(!dark)}>{dark ? 'Light theme' : 'Dark theme'}</Button>
</div>
<Body1>Project brief and access settings · local demonstration</Body1>
<TabList aria-label="Workspace views" selectedValue={tab}
onTabSelect={(_, data) => setTab(data.value)}>
<Tab id="brief-tab" value="brief" aria-controls="brief-panel">Brief</Tab>
<Tab id="people-tab" value="people" aria-controls="people-panel">People</Tab>
</TabList>
<section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab"
hidden={tab !== 'brief'} className={styles.panel} tabIndex={0}>
<h2>Launch a clearer onboarding flow</h2>
<p>Review the welcome screen and first-project setup before the team walkthrough.</p>
<p>Decision needed: should the sample project be created automatically or offered as an option?</p>
</section>
<section id="people-panel" role="tabpanel" aria-labelledby="people-tab"
hidden={tab !== 'people'} className={styles.panel} tabIndex={0}>
<h2>People with access</h2>
<ul>{people.map(person => <li key={person.email} className={styles.row}>
<span>{person.email}</span><strong>{person.permission}</strong>
</li>)}</ul>
</section>
<Dialog open={open} onOpenChange={(_, data) => setOpen(data.open)}>
<DialogTrigger disableButtonEnhancement><Button appearance="primary">Add a person</Button></DialogTrigger>
<DialogSurface>
<form noValidate onSubmit={submit}>
<DialogBody>
<DialogTitle>Add a person to Launch workspace</DialogTitle>
<DialogContent className={styles.form}>
<p>This example updates local state only. It does not send email or change real access.</p>
<Field label="Email address" required validationState={error ? 'error' : 'none'}
validationMessage={error || undefined}>
<Input type="email" value={email} onChange={(_, data) => { setEmail(data.value); setError(''); }} />
</Field>
<Field label="Permission" hint="Viewers can read; editors can change the brief in a real application">
<Select value={permission} onChange={(_, data) => setPermission(data.value)}>
<option>Can view</option><option>Can edit</option>
</Select>
</Field>
</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement><Button type="button">Cancel</Button></DialogTrigger>
<Button type="submit" appearance="primary">Add locally</Button>
</DialogActions>
</DialogBody>
</form>
</DialogSurface>
</Dialog>
<p role="status">{notice}</p>
</main>;
}
function App() {
const [dark, setDark] = React.useState(false);
return <FluentProvider theme={dark ? webDarkTheme : webLightTheme}>
<Workspace dark={dark} setDark={setDark} />
</FluentProvider>;
}
createRoot(document.getElementById('root')).render(<App />);
コードリファレンス
- 既存の React アプリ向けに、React v9 のコンポーネントを完全に組み合わせた例です。スタンドアロンのプレビューは、別個の HTML/CSS による解釈です。このコードで行うのは、ローカルでの入力検証と状態更新のみです。メール送信や実際の権限変更は行いません。ソース API は確認済みですが、対象プロジェクトでテストするまでは、ネイティブパッケージのコンパイルを確認したとはしていません。
Field + Input + Select:ラベル付きのアクセスフォーム
以下のコード片は、後述の完全なセットアップにある状態変数とインポートを使用します。Field がラベル、ヒント、検証結果の表示を担い、メールアドレスのルールと重複チェックはアプリケーション側で処理します。
jsx
<Field label="Email address" required
validationState={error ? 'error' : 'none'}
validationMessage={error || undefined}>
<Input type="email" value={email}
onChange={(_, data) => { setEmail(data.value); setError(''); }} />
</Field>
<Field label="Permission" hint="Choose what this person can do">
<Select value={permission}
onChange={(_, data) => setPermission(data.value)}>
<option>Can view</option><option>Can edit</option>
</Select>
</Field>コードリファレンス
- required はアクセシブルな必須項目の表示を行いますが、アプリケーションの検証ルールを実装するものではありません。
- validationMessage がある場合、既定では検証状態が error になります。エラーを表示しない場合は、none を明示的に設定してください。
- Input の onChange では、現在の文字列が data.value に渡されます。ハンドラーを、DOM イベントのみを受け取る通常の入力ハンドラーと混同しないでください。
パネルとの関連付けを明示した、制御可能な TabList
選択状態とタブの動作は TabList が制御します。コンテンツパネルは独自にレンダリングし、各 Tab の ID とアクセシブルな名前を対応するパネルに関連付けてください。
jsx
<TabList selectedValue={tab} aria-label="Workspace views"
onTabSelect={(_, data) => setTab(data.value)}>
<Tab id="brief-tab" value="brief" aria-controls="brief-panel">Brief</Tab>
<Tab id="people-tab" value="people" aria-controls="people-panel">People</Tab>
</TabList>
<section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab"
hidden={tab !== 'brief'} tabIndex={0}>Project brief</section>
<section id="people-panel" role="tabpanel" aria-labelledby="people-tab"
hidden={tab !== 'people'} tabIndex={0}>People with access</section>コードリファレンス
- selectedValue は TabList に設定し、各 Tab には個別の値を設定します。
- selectTabOnFocus の既定値は false です。この動作を明示的に変更しない限り、矢印キーによるフォーカス移動と選択は別々に行われます。
- Fluent の横並びタブは、自動的に折り返されたりスクロールしたりしません。この例では、常に表示できるよう短いラベルを 2 つ使用しています。
04Microsoft Fluent 2 デザインシステムのエージェント設定
実装の出発点
既存の React アプリ向けに、React v9 のコンポーネントを完全に組み合わせた例です。スタンドアロンのプレビューは、別個の HTML/CSS による解釈です。このコードで行うのは、ローカルでの入力検証と状態更新のみです。メール送信や実際の権限変更は行いません。ソース API は確認済みですが、対象プロジェクトでテストするまでは、ネイティブパッケージのコンパイルを確認したとはしていません。
jsx
// Terminal: npm install @fluentui/react-components@9
// Existing React application; index.html must contain <div id="root"></div>.
// Entry file: src/main.jsx
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import {
FluentProvider, webLightTheme, webDarkTheme, makeStyles, tokens,
Button, Body1, Title3, TabList, Tab, Field, Input, Select,
Dialog, DialogTrigger, DialogSurface, DialogBody, DialogTitle,
DialogContent, DialogActions
} from '@fluentui/react-components';
const useStyles = makeStyles({
page: { maxWidth: '880px', margin: '0 auto', padding: tokens.spacingHorizontalXXL },
header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: tokens.spacingHorizontalL },
panel: { padding: tokens.spacingHorizontalXXL, marginTop: tokens.spacingVerticalL,
backgroundColor: tokens.colorNeutralBackground2, borderRadius: tokens.borderRadiusXLarge },
form: { display: 'grid', gap: tokens.spacingVerticalL },
row: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: tokens.spacingHorizontalM,
paddingTop: tokens.spacingVerticalM, paddingBottom: tokens.spacingVerticalM },
});
function Workspace({ dark, setDark }) {
const styles = useStyles();
const [tab, setTab] = React.useState('brief');
const [open, setOpen] = React.useState(false);
const [email, setEmail] = React.useState('');
const [permission, setPermission] = React.useState('Can view');
const [error, setError] = React.useState('');
const [notice, setNotice] = React.useState('');
const [people, setPeople] = React.useState([{ email: '[email protected]', permission: 'Owner' }]);
function submit(event) {
event.preventDefault();
const value = email.trim().toLowerCase();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
setError('Enter an email address such as [email protected]'); return;
}
if (people.some(person => person.email === value)) {
setError('This address already appears in the local access list'); return;
}
setPeople(previous => [...previous, { email: value, permission }]);
setNotice(`Added ${value} as ${permission.toLowerCase()} in this local example. No invitation was sent.`);
setEmail(''); setError(''); setOpen(false); setTab('people');
}
return <main className={styles.page}>
<div className={styles.header}>
<Title3 as="h1">Launch workspace</Title3>
<Button onClick={() => setDark(!dark)}>{dark ? 'Light theme' : 'Dark theme'}</Button>
</div>
<Body1>Project brief and access settings · local demonstration</Body1>
<TabList aria-label="Workspace views" selectedValue={tab}
onTabSelect={(_, data) => setTab(data.value)}>
<Tab id="brief-tab" value="brief" aria-controls="brief-panel">Brief</Tab>
<Tab id="people-tab" value="people" aria-controls="people-panel">People</Tab>
</TabList>
<section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab"
hidden={tab !== 'brief'} className={styles.panel} tabIndex={0}>
<h2>Launch a clearer onboarding flow</h2>
<p>Review the welcome screen and first-project setup before the team walkthrough.</p>
<p>Decision needed: should the sample project be created automatically or offered as an option?</p>
</section>
<section id="people-panel" role="tabpanel" aria-labelledby="people-tab"
hidden={tab !== 'people'} className={styles.panel} tabIndex={0}>
<h2>People with access</h2>
<ul>{people.map(person => <li key={person.email} className={styles.row}>
<span>{person.email}</span><strong>{person.permission}</strong>
</li>)}</ul>
</section>
<Dialog open={open} onOpenChange={(_, data) => setOpen(data.open)}>
<DialogTrigger disableButtonEnhancement><Button appearance="primary">Add a person</Button></DialogTrigger>
<DialogSurface>
<form noValidate onSubmit={submit}>
<DialogBody>
<DialogTitle>Add a person to Launch workspace</DialogTitle>
<DialogContent className={styles.form}>
<p>This example updates local state only. It does not send email or change real access.</p>
<Field label="Email address" required validationState={error ? 'error' : 'none'}
validationMessage={error || undefined}>
<Input type="email" value={email} onChange={(_, data) => { setEmail(data.value); setError(''); }} />
</Field>
<Field label="Permission" hint="Viewers can read; editors can change the brief in a real application">
<Select value={permission} onChange={(_, data) => setPermission(data.value)}>
<option>Can view</option><option>Can edit</option>
</Select>
</Field>
</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement><Button type="button">Cancel</Button></DialogTrigger>
<Button type="submit" appearance="primary">Add locally</Button>
</DialogActions>
</DialogBody>
</form>
</DialogSurface>
</Dialog>
<p role="status">{notice}</p>
</main>;
}
function App() {
const [dark, setDark] = React.useState(false);
return <FluentProvider theme={dark ? webDarkTheme : webLightTheme}>
<Workspace dark={dark} setDark={setDark} />
</FluentProvider>;
}
createRoot(document.getElementById('root')).render(<App />);
あなたのプロジェクト向けプロンプト
AI エージェント用プロンプト
@fluentui/react-components の Fluent UI React v9 を使用します。webLightTheme を設定した FluentProvider をレンダリングし、webDarkTheme に切り替えられるようにします。概要には Title3 と Body1 を使い、TabList とビューを制御可能な形で組み合わせ、各パネルを明示します。アクセス用 Dialog には Field、Input、Select と実際のフォームを含めます。独自の背景レイヤーと余白にはトークンを使用します。提示されたコンポーネント例とランタイムの型ペアに従い、Subtitle1 は 20/28 とします。無効な入力を保持し、ローカルでのアドレス重複を拒否し、ダイアログのフォーカスを復元し、ローカルでの追加を実態に即して説明します。認可と結果の契約が提示された場合に限り、共有サービスと連携します。
05Microsoft Fluent 2 デザインシステムの出典と対象範囲
Microsoft の製品でも、実際に利用できる共有サービスでもなく、OpenDesign アプリケーションを対象にした Fluent UI React v9 の検証です。スタンドアロンのプレビューは、検証済みの Web トークンとコンポーネント構造を HTML/CSS で解釈したものです。以下の React コードでは Fluent のネイティブコンポーネントを使用します。参照値は 2026 年 10 月 3 日に確認したリンク先のソーススナップショットに基づき、すべてのリリース、プラットフォーム、ブランドで同一の値が使われることを保証するものではありません。