Skip to content

API proposal for Label support for DisplayName #65062

@ilonatommy

Description

@ilonatommy

Background and Motivation

Blazor currently lacks built-in mechanisms to display property names from metadata attributes. While MVC and Razor Pages have the @Html.DisplayNameFor() helper, Blazor developers were forced to either:

  • Hardcode label text (violating DRY principles and making localization difficult)
  • Build custom reflection-based solutions
  • Duplicate display name information across models and views

Additionally, after introducing DisplayName<TValue>, there was no built-in way to render proper HTML <label> elements that automatically associate with form inputs using either the nested pattern (wrapping) or the non-nested pattern (for/id matching).

These changes provide attribute-based solutions that follow the same familiar pattern as other Blazor form components like ValidationMessage<TValue>, enabling proper accessibility and reducing boilerplate code.

References:


Proposed API

namespace Microsoft.AspNetCore.Components.Forms;

+ public class DisplayName<TValue> : ComponentBase
+ {
+     [Parameter]
+     [EditorRequired]
+     public Expression<Func<TValue>>? For { get; set; }
+ }

+ public class Label<TValue> : IComponent
+ {
+     public Label();
+     
+     [Parameter]
+     [EditorRequired]
+     public Expression<Func<TValue>>? For { get; set; }
+     
+     [Parameter]
+     public RenderFragment? ChildContent { get; set; }
+     
+     [Parameter(CaptureUnmatchedValues = true)]
+     public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
+ }

public abstract class InputBase<TValue>
{
+     protected string IdAttributeValue { get; }
}

Usage Examples

DisplayName Component

Basic form usage:

<EditForm Model="@product">
    <div class="mb-3">
        <label class="form-label">
            <DisplayName For="@(() => product.Name)" />
        </label>
        <InputText @bind-Value="product.Name" class="form-control" />
        <ValidationMessage For="@(() => product.Name)" />
    </div>
</EditForm>

Renders as:

<label class="form-label">Product Name</label>

Table headers:

<table class="table">
    <thead>
        <tr>
            <th><DisplayName For="@(() => product.Name)" /></th>
            <th><DisplayName For="@(() => product.Price)" /></th>
            <th><DisplayName For="@(() => product.ReleaseDate)" /></th>
        </tr>
    </thead>
</table>

Label Component

Nested pattern (wrapping):

<Label For="() => model.Name">
    <InputText @bind-Value="model.Name" />
</Label>

Renders:

<label>
    Name
    <input name="model.Name" ... />
</label>

Non-nested pattern (for/id):

<Label For="() => model.Name" />
<InputText @bind-Value="model.Name" />

Renders:

<label for="model.Name">Name</label>
<input id="model.Name" name="model.Name" ... />

Model example:

public class Product
{
    [Display(Name = "Product Name")]
    public string Name { get; set; }
    
    [DisplayName("Unit Price")]
    public decimal Price { get; set; }
    
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }
}

Alternative Designs

For Label component:

Two proposals were considered for the Label component:

  1. Non-nested pattern only (<label for="..."> matching <input id="...">): This approach required adding id attributes to all input components and could cause breaking changes for users who manually provided id attributes that differ from name.

  2. Nested pattern (preferred, implemented first): Wrapping input inside label provides implicit HTML association without requiring for/id matching. This approach has no breaking changes and lower implementation complexity.

The final implementation supports both patterns: nested (when ChildContent is provided) and non-nested (when no ChildContent, using for/id association).

For DisplayName component:

The component name was originally DisplayNameLabel, but was renamed to DisplayName to avoid misleading users into thinking it would render a <label> element (it only renders the plain display name text).


Risks

  1. Label + Input coordination (non-nested pattern): The Label and Input components are two separate framework components that need to coordinate. If a user overrides the id attribute on the input to a value that doesn't match what Label generates for its for attribute, the association will break. This is documented and users who explicitly provide id/for attributes take responsibility for matching them correctly.

  2. Breaking changes for non-nested pattern: Users who previously manually provided id attributes that differ from the auto-generated name values may need to update their code when using the non-nested Label pattern.

  3. ID sanitization consideration: The id attribute value should potentially be sanitized to work properly with CSS selectors (e.g., querySelector). The implementation should match what MVC's DefaultHtmlGenerator does for consistency.

  4. HtmlFieldPrefix handling: The Label component needs to account for HtmlFieldPrefix similar to how InputBase handles NameAttributeValue.

  5. Localization: Ensured to work with the component through testing with various localization resources.


Source Justifications

Content Source Quote
Background - Blazor lacks DisplayNameFor "Blazor currently lacks a built-in mechanism to display property names from metadata attributes like MVC's @Html.DisplayNameFor() helper" - PR #64636
Forces developers to hardcode/build custom "This forces developers to either: Hardcode label text (violating DRY principles and making localization difficult), Build custom reflection-based solutions, Duplicate display name information" - PR #64636
DisplayName API "public class DisplayName : ComponentBase { [Parameter] [EditorRequired] public Expression<Func>? For { get; set; } }" - PR #64636
Label API "Microsoft.AspNetCore.Components.Forms.Label, Label.For, Label.ChildContent, Label.AdditionalAttributes" - PR #64821
IdAttributeValue property "Microsoft.AspNetCore.Components.Forms.InputBase.IdAttributeValue.get -> string" - PR #64821
Nested pattern usage "Nested pattern (wrapping): <Label For="() => model.Name"><InputText @bind-Value="model.Name" /></Label>" - PR #64821
Non-nested pattern usage "Non-nested pattern (for/id): <Label For="() => model.Name" /><InputText @bind-Value="model.Name" />" - PR #64821
Alternative designs - two proposals "Proposal 1... Proposal 2 (preferred)" - Issue #64791
Name change from DisplayNameLabel "The name DisplayNameLabel might be missleading. I thought it would render a but it only renders the plain name" - @campersau in PR #64636
Breaking changes risk "The <label for="..."> only works if the input has a matching id attribute... Currently, input renders only name, not id" - Issue #64791
ID sanitization risk "We should check what DefaultHtmlGenerator does... I believe we might need/want to sanitize the id" - @javiercn review in PR #64821
HtmlFieldPrefix risk "We need to account here for the HtmlFieldPrefix I think" - @javiercn review in PR #64821
Localization requirement "We need to ensure localization works with this component" - @javiercn in PR #64636

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-ready-for-reviewAPI is ready for formal API review - https://github.com/dotnet/apireviewsapi-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-blazorIncludes: Blazor, Razor Components

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions