はじめに
フォームのラベル付けは、アクセシビリティの中でもっとも基本で、もっとも壊れやすい部分です。「ラベルらしきテキスト」が入力欄の近くに置いてあるだけでは、スクリーンリーダーの利用者にとって「この入力欄が何を求めているのか」が伝わらないことがあります。プレースホルダーをラベル代わりにしてしまう例は、その典型です。
Fluent UI Blazor v5 には、この「基本だが壊れやすい」部分を引き受ける FluentField というコンポーネントがあります。ラベル・必須表示・検証メッセージ・ヒントテキストをまとめて入力コントロールに付与する、いわばアクセシブルなフォーム部品の土台です。
本記事では、FluentField を「使い方」ではなく ソースコードの流れ から読み解きます。microsoft/fluentui-blazor の dev-v5 ブランチを一次情報として、FluentField 本体・FluentValidationMessage・<label> の関連付け・シャドウ DOM への aria-label コピー・CSS のコントラスト指定までを追いかけ、それぞれが WCAG 2.1 の達成基準のどれを支えているのかを対応づけます。そして最後に、Field を使わない(あるいは正しく渡さない)と、どの経路が動かなくなるのかを整理します。
アクセシビリティの全体像やテスト戦略については、以前書いた記事にまとめてあります。あわせて読むと本記事の位置づけが分かりやすいはずです。
- WCAG 2.2 × Fluent UI で実現するWebアクセシビリティ設計とテスト戦略
- Fluent UI / Fluent 2 で raw 値より alias token を使うべき理由
- Fluent UI Blazor v5 RC1がリリース!v4からの進化と新機能を徹底解説
今回のゴール 🎯
この記事を読み終えると、次のことが分かるようになります。
- ✅
FluentFieldが生成する DOM 構造(<label for>と入力の ID 連鎖)をソースで説明できる - ✅ Fluent の入力コンポーネントで「なぜ
aria-labelをシャドウ DOM にコピーするのか」を理解する - ✅ 必須表示・エラー通知・コントラストが、どの WCAG 2.1 達成基準に対応しているかを言える
- ✅ Field を使わないと破綻する箇所を具体的に指摘できる
- ✅ どのプロパティ(
Label/Required/ForIdなど)を設定しないとアクセシビリティが守れないかを断定できる - ✅ 自動テストで担保できる範囲と、手動・支援技術で確認すべき範囲を分離できる
先に結論を言うと、FluentFieldは「ラベルと入力を確実に結びつけ、テーマに追従した色でエラーを見せる」ための仕組みです。ただし後述するように、v5 の Field はaria-invalid/aria-describedbyを自動では付けません。ここが自動と手動の境界線になります。
前提条件とソースの参照点
本記事は次のバージョン・参照点を前提にしています。
- ✅
Microsoft.FluentUI.AspNetCore.Componentsv5(本記事執筆時点では 5.0.0 の RC 段階で、NuGet 上の最新は5.0.0-rc.5系) - ✅ ソースは
microsoft/fluentui-blazorのdev-v5ブランチ、コミットf17cb66を参照点に固定 - ✅ ライブラリの最小対応フレームワークは
.NET 8(Directory.Build.propsのNetVersion)
以降のソースリンクはすべてこのコミットの blob(行番号付き)に固定しています。ブランチの先端は動くため、再現性のためにコミット SHA でピン留めしています。
ソースの引用は「推測ではなく、その時点のコードにこう書いてある」という意味です。将来のバージョンでは実装が変わる可能性があるため、更新時は同じ経路を再確認してください。
そもそも Field とは何を担うコンポーネントか
FluentField の役割は、コンポーネントの要約コメントに端的に書かれています。
Field adds a label, validation message, and hint text to a control.
(Field はコントロールに、ラベル・検証メッセージ・ヒントテキストを付与する)
-
FluentField.razor.cs
そして重要なのは、FluentTextInputのような Fluent の入力コンポーネント自身が、内部でFluentFieldにラップされているという事実です。FluentTextInput.razorの先頭を見ると分かります。
<FluentField InputComponent="@this" ValueExpression="@ValidationFieldFor" ForId="@Id" Class="@ClassValue" Style="@StyleValue" Size="@Size.ToFieldSize()">
<fluent-text-input @ref="@Element"
...
aria-label="@AriaLabel"
id="@Id"
slot="@FluentSlot.FieldInput"
... />
</FluentField>
つまり Field は「付けても付けなくてもよい飾り」ではなく、入力コンポーネントがアクセシビリティを成立させるための構造そのものです。FluentTextInput Label="..." と書いたときの Label は、この内側の FluentField に渡されています。ここを押さえると、「Field を経由しないと何が失われるか」が具体的に見えてきます。
ソースを読む①:ラベルと入力の関連付け
<label for> が張る関係(1.3.1 / 2.4.6 / 3.3.2)
FluentField.razor の描画本体を見ると、ラベルは素の HTML <label> として出力され、for 属性で入力の ID を指しています。
<label id="@GetId("label")"
slot="@FluentSlot.FieldLabel"
for="@GetId("input")"
required="@(Parameters.Required ?? false)"
disabled="@(Parameters.Disabled ?? false)"
style="@LabelStyle">
...
@Parameters.Label
@Parameters.LabelTemplate
</label>
for="@GetId("input")" と、入力側の id="@GetId("input")" が 同じ ID を共有します。これが HTML の標準的なラベル関連付けです。WHATWG HTML 仕様は for 属性について次のように定義しています。
The
forattribute may be specified to indicate a form control with which the caption is to be associated.
この「見た目のラベルとコントロールをプログラム的に結びつける」ことは、WCAG 2.1 の複数の達成基準の土台になります。
1.3.1 Info and Relationships(Level A): Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.
3.3.2 Labels or Instructions(Level A): Labels or instructions are provided when content requires user input.
さらに、ラベルの内容が目的を説明していることは 2.4.6 が求めています。
2.4.6 Headings and Labels(Level AA): Headings and labels describe topic or purpose.
ここで強調したいのは、プレースホルダーはラベルの代替にならないという点です。WHATWG HTML 仕様自身が明言しています。
The placeholder attribute should not be used as an alternative to a label.
FluentField は Label(または LabelTemplate)を明示的に <label for> として出力するため、この落とし穴を構造的に避けられます。逆に言えば、Label を渡さず Placeholder だけで済ませると、この保証は効きません。
ID を生成する GetId の流れ
for と id が確実に一致するのは、両方を同じメソッド GetId が生成しているからです。
internal string? GetId(string slot)
{
// Wrapper of an FieldInput component
if (Parameters.HasInputComponent)
{
var id = (string.IsNullOrEmpty(ForId) ? Id : ForId) ?? _defaultId;
return slot switch
{
"field" => $"{id}-field",
"input" => $"{id}",
"label" => $"{id}-label",
_ => throw new ArgumentException($"Invalid slot: {slot}"),
};
}
// Standalone FluentField
return slot switch
{
"field" => Id,
"input" => string.IsNullOrEmpty(ForId) ? $"{Id ?? _defaultId}-input" : ForId,
"label" => string.IsNullOrEmpty(Id) ? null : $"{Id}-label",
_ => throw new ArgumentException($"Invalid slot: {slot}"),
};
}
ポイントは 2 つです。
-
ID が未指定でも
_defaultId(Identifier.NewId())で自動採番されるので、開発者が ID を書き忘れてもfor/idの対応は崩れません。 - Fluent の入力コンポーネントをラップする場合(
HasInputComponent)は入力の ID がそのままid、スタンドアロン利用では"{Id}-input"になる、というようにモードによって命名規則を切り替えています。
この一貫した ID 生成があるからこそ、<label for>の関連付けが破綻しません。ここは「自動でチェックできる範囲」の代表格です(後述)。
公式ドキュメントのベストプラクティスには「1 つの Field に複数のコントロールを子として入れない。ラベルは 1 つのコントロールにしか関連付かない」と明記されています。for は 1 つの id しか指せないため、この制約はソースの構造そのものから来ています。
ソースを読む②:シャドウ DOM の壁と aria-label コピー
ここが v5 の Field でもっとも「読む価値がある」箇所です。
Fluent の入力コンポーネント(FluentTextInput / FluentTextArea / FluentNumberInput など)は、内部が Web Components でできています。実際にフォーカスを受ける本物のコントロールは、その Web Component の シャドウ DOM の中 にあります。ところが、<label for> や aria-labelledby のようなライト DOM(通常の DOM)からの関連付けは、シャドウ DOM の境界を越えられません。つまり <label for="input-id"> を書いても、シャドウ内の本物のコントロールには「アクセシブルな名前」が伝わらないのです。
ソースのコメントが、この問題と対処をそのまま説明しています。
// The <label> rendered above targets the wrapped component's host element (e.g. <fluent-text-input>),
// but the real focusable control lives inside that host's shadow DOM and has no accessible name of its
// own: light-DOM `for`/`aria-labelledby` cannot cross the shadow boundary. Rather than relying on each
// component's internal (and inconsistent) shadow-DOM label, push a plain-text `aria-label` directly onto
// the shadow ".control" element, the same way for every wrapped component.
var ariaLabel = GetControlAriaLabel();
var targetId = GetId("input");
...
await JSRuntime.InvokeFluentVoidAsync("Microsoft.FluentUI.Blazor.Utilities.Attributes.copyToShadow", targetId, ".control", "aria-label", ariaLabel);
呼び出し先の JavaScript は非常に単純で、指定した要素のシャドウルートから .control を探し、そこに属性をセットするだけです。
export function copyToShadow(
elementOrId: HTMLElement | string,
shadowSelector: string,
attributeName: string,
attributeValue: string) {
...
const shadowElement = element.shadowRoot?.querySelector(shadowSelector);
if (shadowElement) {
shadowElement.setAttribute(attributeName, attributeValue);
}
}
つまり Field は、OnAfterRenderAsync の初回描画時に「ライト DOM の <label for>」と「シャドウ DOM の .control への aria-label」の両建てで名前を担保しているわけです。これは WCAG 2.1 の 4.1.2 Name, Role, Value(支援技術がコンポーネントの名前を取得できること)の前提を、シャドウ DOM 越しでも満たすための実装上の工夫です。
この aria-label コピーが対象にするのは IFluentControlAriaLabel を実装する一部の入力コンポーネントだけです。また GetControlAriaLabel() はプレーンテキストの Label からしか名前を作らないため、LabelTemplate や LabelInfo(リッチなラベル)はシャドウの aria-label には反映されません。装飾入りのラベルにする場合は、名前が正しく読み上げられるか支援技術での確認が必要です。
ソースを読む③:必須表示の伝達経路(3.3.2)
必須(required)は、視覚と支援技術の両方に伝わる必要があります。FluentField は 2 経路でこれを扱います。
-
視覚:
<label>にrequired属性を出力する(前掲のrequired="@(Parameters.Required ?? false)")。 -
支援技術(シャドウ制御向け):
aria-labelの末尾にローカライズされた「必須」語を追記する。
後者はGetControlAriaLabelの実装に現れます。
private string? GetControlAriaLabel()
{
var label = (InputComponent as IFluentControlAriaLabel)?.AriaLabel ?? Parameters.Label;
if (string.IsNullOrWhiteSpace(label))
{
return null;
}
return label + (Parameters.Required == true ? $", {Localizer[Localization.LanguageResource.FluentInputBase_Required]}" : string.Empty);
}
FluentInputBase 側にも同じ発想の GetAriaLabelWithRequired() があり、AriaLabel ?? Label にローカライズ済みの必須語を足しています(FluentInputBase.cs #L290)。
ここで大事なのは、必須であることが「色や記号だけ」でなくテキストとして名前に含まれる点です。アスタリスク * だけに頼ると、読み上げ環境によっては意味が伝わりません。名前に「必須」を含めることで、そのリスクを下げています。これは 3.3.2(入力に必要なラベル・説明の提供)の実装的な補強といえます。
ソースを読む④:エラー通知の流れ(3.3.1)
検証エラーの表示は FluentField と FluentValidationMessage の連携で実現します。まず Field 側は、EditContext に検証メッセージがある場合だけ FluentValidationMessage を描画します。
@if (HasValidationMessages)
{
<FluentValidationMessage TValue="object"
Field="@_fieldIdentifier" />
}
HasValidationMessages は、EditContext.GetValidationMessages(fieldIdentifier) が 1 件以上を返すかどうかで決まります(FluentField.razor.cs #L288)。この FieldIdentifier は、ValueExpression(例:@bind-Value に紐づく式)から生成されます。
FluentValidationMessage は、そのメッセージを赤いテキストとエラーアイコン付きで、message スロットに描画します。
<FluentText slot="@FluentSlot.FieldMessage"
...
Size="@TextSize.Size200"
Color="@Color.Error"
@attributes="AdditionalAttributes">
@foreach (var message in ValidationMessages)
{ <div message >
@CreateIcon(Icon)
@message
</div>
}
</FluentText>
デフォルトのアイコンは FluentStatus.ErrorIcon(赤い DismissCircle)です(FluentStatus.cs)。アイコン(色)とテキストの両方でエラーを示すため、色だけに依存しません。エラーの内容がテキストで説明されることは、WCAG の次の基準に対応します。
3.3.1 Error Identification(Level A): If an input error is automatically detected, the item that is in error is identified and the error is described to the user in text.
重要な限界:v5 の Field は aria-invalid / aria-describedby を自動付与しない
ここは誤解しやすいので、ソースで確認した事実として明確に書きます。コミット f17cb66 の src/Core 全体を検索したところ、aria-invalid と aria-errormessage の出力箇所は存在せず、aria-describedby は Toast コンポーネントでしか使われていませんでした(フォーム入力では未使用)。
これが意味するのは次のことです。
- 🟢 エラーは視覚的には十分に伝わる(赤いテキスト+アイコン+赤枠)
- 🟢 エラーメッセージのテキストは DOM 上に存在する(
messageスロット内) - 🔴 しかし、入力要素に
aria-invalid="true"は付かない - 🔴 入力要素からエラーメッセージへの
aria-describedby連鎖も張られない
WAI-ARIA では、aria-invalidは「入力値が期待する形式に適合していないこと」を、aria-describedbyは「その要素を説明する要素(エラーメッセージ等)」を支援技術に伝えるための属性です(aria-invalid | WAI-ARIA 1.2 / aria-describedby | WAI-ARIA 1.2)。これらが自動で付かないということは、「入力欄そのものにフォーカスした状態で、エラーがプログラム的に結びついて読み上げられるか」は保証されないということです。
これは「Fluent UI Blazor v5 がアクセシブルでない」という意味ではありません。エラーの提示は視覚・テキストの両面で行われています。ただし、aria-invalid / aria-describedby によるプログラム的なエラー関連付けを要件に含めるなら、AdditionalAttributes で明示的に付与するか、実機のスクリーンリーダーで読み上げ挙動を必ず確認する必要があります。ここは自動では担保されない「人間が確認すべき領域」です。
ソースを読む⑤:色・コントラスト・タイポグラフィー(1.4.3 / 1.4.11)
FluentField の CSS は短いですが、アクセシビリティ上の要点が詰まっています。
fluent-field label[disabled] {
color: var(--colorNeutralForegroundDisabled);
}
fluent-field.invalid div::part('root') {
border-color: var(--colorPaletteRedBorder2);
}
注目したいのは、色が raw 値(生の 16 進数)ではなく alias/global の design token(--colorPaletteRedBorder2 など)で指定されていることです。エラー時の枠線色を token 経由にしておくと、ライト/ダーク/ハイコントラストのテーマ切り替えでコントラストが破綻しにくくなります。token を使う理由は別記事にまとめました(raw 値より alias token を使うべき理由)。
そして .invalid クラスは、検証メッセージがあるときに Field 側で付与されます。
protected string? ClassValue => DefaultClassBuilder
.AddClass(Configuration.DefaultStyles.FluentFieldClass, when: HasLabel)
.AddClass("invalid", when: HasValidationMessages)
.Build();
この「エラー枠線」や「フォーカスリング」は文字ではない UI の視覚情報なので、コントラスト要件が変わります。文字と UI 部品では、適用される達成基準が異なる点に注意してください。
1.4.3 Contrast (Minimum)(Level AA): The visual presentation of text and images of text has a contrast ratio of at least 4.5:1 ...(大きな文字は 3:1 以上)
1.4.11 Non-text Contrast(Level AA): The visual presentation of the following have a contrast ratio of at least 3:1 against adjacent color(s): User Interface Components ... Graphical Objects ...
-
1.4.11 Non-text Contrast | WCAG 2.1
ここでいうコントラスト比は、2 色の相対輝度から次の式で定義されます。
(L1 + 0.05) / (L2 + 0.05), where L1 is the relative luminance of the lighter of the colors, and L2 is the relative luminance of the darker of the colors.
つまり、
- 🎨 ラベルやエラーメッセージの文字は 4.5:1 以上(大きな文字は 3:1 以上)→ 1.4.3
- 🖼️ エラー時の枠線やフォーカスリングなどの UI 部品は 3:1 以上 → 1.4.11
を満たす必要があります。disabledなラベルの色(--colorNeutralForegroundDisabled)のように、意図的に薄くする色は「無効な UI 部品の一部」として文字コントラストの対象外になりますが、「本当に無効状態か」を含めて設計者の判断が要ります。
「1.4.3 の大きな文字」は WCAG 2.1 の定義では 18pt もしくは太字 14pt 以上(large scale の定義)を指します。px 換算(24px / 18.66px 相当)はよく使われる目安ですが、規定文そのものは pt で書かれている点に注意してください。
タイポグラフィーと間隔
Field はスペーシングにも既定値を持っています。ラベルやメッセージを含む Field には my-3-o クラスが付き、上下に 12px(合計で隣接フィールド間 24px)の余白が入ります。これは Fluent UI のデザイナー推奨値で、AddFluentUIComponents のオプションで差し替えも可能です(公式ドキュメントの Field 解説より)。適切な行間・間隔は可読性に直結し、1.4.12 Text Spacing(本記事の主対象外ですが関連します)とも相性が良い設計です。
単体利用で「破綻」するのはどこか
ここまでの流れを、①〜⑤で見た各経路を「Field を使わない/正しく渡さない」と何が失われるか、という観点で総括します。
| 失われるもの | 破綻の内容 | 関係する達成基準 |
|---|---|---|
| 🏷️ ラベル関連付け |
<label for> と id の対応が消え、ラベルが入力に紐づかない |
1.3.1 / 2.4.6 / 3.3.2 |
| 🔊 シャドウ制御の名前 | シャドウ DOM の .control に aria-label が渡らず、名前が読み上げられない |
4.1.2 の前提 |
| * 必須表示 |
required 属性と「必須」語の付与が消え、必須が色/記号頼みになる |
3.3.2 |
| ⚠️ エラー通知 |
EditContext からの検証メッセージ描画(テキスト+アイコン)が出ない |
3.3.1 |
| 🎨 テーマ追従の色 |
.invalid の枠線 token(--colorPaletteRedBorder2)が付かず、コントラストが不定 |
1.4.11 |
| 🆔 ID/aria の一貫性 |
GetId による安定した ID 生成がなくなり、関連付けが自作依存になる |
1.3.1 |
特に強調したいのは、プレースホルダーをラベル代わりにする、あるいは素の <fluent-text-input> を Field なしで直接置くと、上記が一気に崩れる点です。Fluent の入力コンポーネントは自分自身を Field にラップしているので、通常は FluentTextInput を使えばこの土台が付いてきます。逆に「独自の入力を Field に載せる」ときは、InputComponent / ForId(またはスタンドアロンの Id)を正しく渡すことが条件になります。
実用コード例(v5 API)
ここからは、ソースで確認した v5 の API だけを使った実用例です。存在しない API は使いません。
例①:EditForm と FluentTextInput で検証まで通す
もっとも標準的な形です。FluentTextInput は内部で FluentField にラップされ、Label / Required / 検証メッセージがまとめて機能します。
@using System.ComponentModel.DataAnnotations
<EditForm Model="@_model" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<FluentTextInput @bind-Value="_model.Email"
Label="メールアドレス"
Placeholder="you@example.com"
Required="true"
TextInputType="TextInputType.Email" />
<FluentButton Type="ButtonType.Submit" Appearance="ButtonAppearance.Accent">
送信
</FluentButton>
</EditForm>
@code {
private readonly SignUpModel _model = new();
private void HandleValidSubmit()
{
// 検証を通過したときの処理
}
public class SignUpModel
{
[Required(ErrorMessage = "メールアドレスは必須です。")]
[EmailAddress(ErrorMessage = "メールアドレスの形式が正しくありません。")]
public string? Email { get; set; }
}
}
[Required] / [EmailAddress] の検証に失敗すると、EditContext の検証メッセージが FluentField 経由で FluentValidationMessage として表示されます。Label を渡しているので <label for> の関連付けも成立します。Placeholder はあくまで補助で、ラベルの代替ではない点に注意してください。
Blazor のフォームと検証の基礎は公式ドキュメントが詳しいです。
例②:任意のコントロールを FluentField でラップする
ネイティブ入力や独自コンポーネントに、ラベル・ヒント・必須表示を付けたいときは Field を直接使います。ライト DOM の入力なので <label for> はそのまま効きます。
<FluentField Label="表示名"
Required="true"
Message="公開プロフィールに表示されます。"
MessageState="MessageState.Warning"
MessageCondition="@FluentFieldCondition.Always">
<InputText @bind-Value="_displayName" class="my-input" />
</FluentField>
@code {
private string _displayName = string.Empty;
}
MessageCondition を指定しないとメッセージは表示されません。常に出したい場合は FluentFieldCondition.Always を渡します(これは公式ドキュメントでも明記されている必須の指定です)。ここで使った MessageState と MessageCondition の詳しい挙動は、次の例③で扱います。
例③:入力に応じてメッセージの状態を切り替える
先ほどの「ソースを読む④:エラー通知の流れ」で見た EditContext ベースの検証とは別に、Field は入力内容に応じて UI 上のフィードバックを出す経路(MessageState / MessageCondition)も持っています。この系統は検証結果の「表示」を担うもので、フォームの正しさそのものを判定する検証ロジックとは別物です。
MessageState は Error / Success / Warning を取り、それぞれアイコンと色の組み合わせでフィードバックします。色だけでなくアイコンとテキストが変わるため、色覚に依存しない伝達になります。
<FluentTextInput @bind-Value="_password"
Label="パスワード"
TextInputType="TextInputType.Password"
Immediate="true"
MessageCondition="@(field => field
.When(() => _password.Length < 8)
.Display("8文字以上にしてください", MessageState.Error)
.When(() => !_password.Any(char.IsDigit))
.Display("数字を1文字以上含めてください", MessageState.Error)
.When(() => true)
.Display("十分な強度です", MessageState.Success)
.Build())" />
@code {
private string _password = string.Empty;
}
When(...).Display(...).Build() は IFluentField の拡張メソッドで、FluentFieldExtensions に実装されています(FluentFieldExtensions.cs)。ルールは定義順に評価されます。
MessageState は表示上のフィードバックであり、EditContext による本来の検証(DataAnnotationsValidator など)とは別物です。フォームの正しさを担保するのは検証側で、MessageState はあくまで UI 表現です。
自動で担保できる範囲と、人間が確認すべき範囲
アクセシビリティは「自動テストを通せば終わり」ではありません。ここまで読んだソースの挙動を、担保方法で分けて整理します。
| 観点 | 主に自動で担保できるか | 確認方法 |
|---|---|---|
🏷️ <label for> と id の一致 |
🟢 できる | DOM スナップショット / axe などの自動チェック |
| 🆔 ID の重複・欠落 | 🟢 できる | 自動アクセシビリティ検査、E2E の DOM 検証 |
| 🎨 文字コントラスト(1.4.3) | 🟢 概ねできる | 自動コントラスト計測(token を使う限り安定) |
| 🖼️ 非テキストコントラスト(1.4.11) | 🟡 一部できる | 枠線・フォーカスリングは自動計測が難しい場合があり目視併用 |
🔊 シャドウ制御の aria-label
|
🟡 一部できる | シャドウ DOM を貫く検査は難しく、実機読み上げで確認 |
⚠️ エラーのプログラム的関連付け(aria-invalid/aria-describedby) |
🔴 v5 では自動で付かない | スクリーンリーダーで実際の読み上げを確認、必要なら手当て |
| 🧭 ラベル文言が目的を説明しているか(2.4.6) | 🔴 できない | 人間のレビュー(文言の妥当性は機械判定不可) |
| 🗣️ 実際の読み上げ体験 | 🔴 できない | NVDA / VoiceOver / ナレーター等での手動確認 |
自動テストで検出できるのは、経験的に全体の一部にとどまります。特に「ラベルの文言が適切か」「エラーが入力欄と結びついて読み上げられるか」は、人間と支援技術による確認が欠かせません。この記事で見た aria-invalid / aria-describedby の話は、まさにその境界にあるテーマです。
コンポーネントライブラリを使うと「アクセシビリティは自動で完璧」と思いがちですが、FluentField のソースを読むと、担保しているのは関連付けと色の土台までだと分かります。プログラム的なエラー通知や文言の妥当性は、利用側の責任として残ります。ライブラリ任せにせず、実機で確かめる文化を持ちたいところです。
おわりに
FluentField を「使い方」ではなく「ソースの流れ」で読むと、アクセシビリティがどこで担保され、どこで担保されないのかがくっきり見えてきます。
-
<label for>とGetIdによる ID 連鎖で、ラベル関連付け(1.3.1 / 2.4.6 / 3.3.2)を構造的に守る - シャドウ DOM を越えられない
for/aria-labelledbyの代わりに、.controlへaria-labelをコピーして名前を担保する - 必須は「色/記号」だけでなくテキストとして名前に含める
- エラーはテキスト+アイコン+テーマ追従の枠線 token で示す(1.4.3 / 1.4.11 / 3.3.1)
- ただし v5 では
aria-invalid/aria-describedbyを自動付与しないため、プログラム的なエラー通知は自分で確認・補完する
結論:どのプロパティを設定しないとアクセシビリティが守れないか
ここまでの読み解きを踏まえて、断定します。FluentField(コミット f17cb66)でアクセシビリティを守れるかどうかは、次の最小セットを実在する v5 API で正しく渡しているかで決まります。
-
Label(またはLabelTemplate)を必ず与える。 これがないと<label for>が出力されず、シャドウ DOM の.controlへのaria-labelコピーも走らないため、入力に「名前」が付きません。プレースホルダーは代替になりません。 -
独自入力をラップするときは、
ForId(またはId)とスロット・ID をラベルのfor先に一致させる。GetIdの起点を誤るとfor/idがずれ、関連付けが切れます。 -
必須入力には
Requiredを、エラーを扱うならEditContext経由の検証(FluentValidationMessageのField/For)を設定する。 どちらも欠くと、必須やエラーがテキストとして支援技術に伝わりません。
逆に、RequiredMessage/ValidationMessage/ValidationErrors/Description/Orientation/Spacingは v5 のFluentFieldには存在しません。これらを「設定すればアクセシビリティが担保される」と考えるのは誤りで、実在する代替(後述)に置き換える必要があります。
推奨設定の 3 分類(v5 実在 API のみ)
これは「単体利用で破綻するのはどこか」で見た「失われる機能」を、プロパティ単位に落とし込んで整理したものです。各プロパティは microsoft/fluentui-blazor の dev-v5(コミット f17cb66)の FluentField.razor.cs / IFluentField.cs / FluentValidationMessage.razor.cs で実在を確認したものだけを挙げています。アクセシビリティを守れるか否かを分けるのは 🔴 と 🟡 の行で、🟢 の行は設定しなくても基本のアクセシビリティは保てる補足的な項目です。
| 分類 | プロパティ(v5 実在 API) | 役割 | 設定しない場合に現れる WCAG 違反リスク |
|---|---|---|---|
| 🔴 最低限必須 |
Label(string?)/代替に LabelTemplate(RenderFragment?) |
ラベルを <label for> として出力し、プレーンテキストの Label をシャドウ .control の aria-label にコピー |
未設定だと入力に名前が結びつかず、プレースホルダー頼みになる → 1.3.1(関係のプログラム的判定不能)/2.4.6(目的を説明するラベルの欠如)/3.3.2(ラベル・説明の欠如) |
| 🔴 最低限必須 |
ForId(string?)/スタンドアロン時は Id
|
<label for> と入力 id を一致させる GetId の起点 |
未指定でも自動採番されるが、独自入力に誤った ID を渡すと for/id が不一致 → 1.3.1(ラベルとコントロールの関連付けが切れる)
|
| 🔴 最低限必須 |
ChildContent(RenderFragment?)+ IncludeInputSlot(bool, 既定 true) |
子入力を slot="input" に載せる。IncludeInputSlot=false のときは自作入力に id="@Id" と slot="@FluentSlot.FieldInput" を自分で付与する必要がある |
スロット・ID を外すとラベルの for 先が実入力に一致せず、名前が付かない → 1.3.1/4.1.2(Name, Role, Value)の前提が崩れる
|
| 🟡 条件付き必須 |
Required(bool?)※必須入力のとき |
<label> に required を出力し、aria-label 末尾へローカライズ済み「必須」語を追記 |
未設定だと必須が色・記号(*)頼みになり、読み上げに必須が伝わらない → 3.3.2
|
| 🟡 条件付き必須 |
EditContext + DataAnnotationsValidator(→ 内部で FluentValidationMessage の Field / For)※エラーを扱うとき |
検証メッセージをテキスト+エラーアイコンで提示 | 未設定だとエラーがテキストで識別・説明されない → 3.3.1(エラーの識別) |
| 🟡 条件付き必須 |
Message(string?)/MessageState(MessageState?)/MessageCondition(Func<IFluentField,bool>?)※UI フィードバックを出すとき |
状態に応じて色+アイコン+テキストを提示(MessageCondition 未指定だと非表示) |
未設定だと入力状態のフィードバックが伝わらない → 3.3.1 の補強(色のみに依存させない) |
| 🟡 条件付き必須 |
aria-invalid / aria-describedby(AdditionalAttributes で手動付与)※プログラム的なエラー関連付けが要るとき |
入力欄自身にエラー状態とメッセージ参照を結ぶ | v5 は自動付与しないため未手当てだと、入力にフォーカスした状態でエラーがプログラム的に読み上げられない → 3.3.1 の支援技術面が非保証 |
| 🟢 装飾/任意 |
LabelPosition(LabelPosition?)/LabelWidth(string?)/Size(FieldSize?) |
ラベル位置・幅・サイズのレイアウト | それ自体はアクセシビリティに直接影響しないが、極端な指定は文字・UI 部品のコントラストを損ないうる → 1.4.3(文字 4.5:1)/1.4.11(UI 部品 3:1)に留意 |
| 🟢 装飾/任意 |
MessageIcon/MessageTemplate/LabelInfo/FieldStartTemplate/FieldEndTemplate
|
アイコン・リッチ表現・補足情報 | 任意。ただし LabelTemplate / LabelInfo はプレーンテキストでないため、シャドウ .control の aria-label には反映されない(名前は Label から生成される) |
存在しない名前と、実在する代替
要件で名前を確認するよう指定された API のうち、v5 の FluentField に存在しないものは次のとおりです。実在 API に置き換えてください。
| よくある誤り | v5 での実在 | 実在する代替・正しい書き方 |
|---|---|---|
RequiredMessage |
❌ FluentField のパラメーターでも IFluentField のメンバーでもない |
必須は Required で伝え、必須時のメッセージは検証([Required(ErrorMessage = "…")] → FluentValidationMessage)か Message で出す |
ValidationMessage / ValidationErrors
|
❌ Fluent のコンポーネント・プロパティ名ではない(ValidationMessage<T> は Blazor 標準の別物) |
FluentValidationMessage<TValue> の Field(FieldIdentifier?)または For(Expression<Func<TValue>>?)。Field が優先される |
Description |
❌ FluentField のパラメーターではない |
ヒント・説明テキストは Message(必要に応じ MessageIcon / MessageTemplate)。なお aria-describedby は自動生成されない |
Orientation |
❌ FluentField のパラメーターではない |
ラベルの向きは LabelPosition(LabelPosition enum) |
Spacing |
❌ FluentField のパラメーターではない |
ラベル幅は LabelWidth、サイズは Size(FieldSize)、余白は基底 FluentComponentBase 由来の Margin / Padding(string?、FluentField にも継承される)で指定する |
まとめると、アクセシビリティの成否を分ける必須プロパティは Label です(独自入力をラップするときは ForId/Id とスロット・ID の一致も必要)。必須やエラーを扱う場面では、Required と EditContext ベースの検証(FluentValidationMessage の Field/For)が条件付きで必須になります。加えて aria-invalid / aria-describedby は v5 では自動で付かないため、プログラム的なエラー関連付けを要件に含めるなら AdditionalAttributes での手当てが要ります。存在しない RequiredMessage / ValidationMessage / ValidationErrors / Description / Orientation / Spacing に頼らないことが、確実にアクセシビリティを守る出発点です。
Field は「アクセシビリティを壊れにくくする土台」であって、「アクセシビリティを完成させる魔法」ではありません。土台の上に、文言の吟味と実機での読み上げ確認を積むことで、初めて WCAG 2.1 AA が現実に守られます。ソースを一度読んでおくと、レビューでも運用でも判断がぶれなくなります。ぜひ手元の dev-v5 を開いて、同じ経路を追いかけてみてください。