Repository navigation
Drop the HTML5 required attribute once MudBlazor honours caller-supplied aria-required #263
Description
Activity
- addedtype:featureNew feature or enhancement requestNew feature or enhancement requestpriority:lowNice to haveNice to havestatus:blockedBlocked by external dependencyBlocked by external dependency
on Aug 12, 2026 Upstream PR opened
MudBlazor/MudBlazor#13613 — MudBlazor/MudBlazor#13613
Input: Let UserAttributes override the computed ARIA attributes, targeting theirdevbranch from
phmatray:fix/mudinput-user-attributes-aria-override.That closes Task 1 of this issue's plan. Tasks 2-4 stay blocked until the PR is merged and a
MudBlazor release carries it — the version pin has to move before the FormCraft-side flip can go green.The patch is unchanged from the one described above:
GetInputUserAttributes()applies the computed
ARIA values withTryAddso a caller wins, mirroringGetDisplayUserAttributes();requiredstays
bound to the parameter, which is what makes the pair separable.If the PR is rejected or stalls, closing this issue as "won't do" and keeping today's option-A
behaviour is a legitimate outcome — it ships correct accessibility already, just with the HTML5
attribute attached.Upstream mechanism changed — the gate and the consumer contract did not.
MudBlazor/MudBlazor#13613 went through review and no longer adds a
GetInputUserAttributes()helper.
The fallbacks are now expressed by attribute ordering instead: thearia-*literals move above the
@attributes="UserAttributes"splat, so last-write-wins resolves in the caller's favour, with no
per-render dictionary.requiredstays below the splat and remains non-overridable.So the parts of this plan that describe the upstream implementation (the
GetInputUserAttributes()
dictionary in the Why this is fixable section and the Interfaces line) are stale. Nothing else
changes:- The gate is the same — this stays blocked until #13613 merges and ships in a MudBlazor release.
- What FormCraft has to do is the same — pass
aria-required="true"throughAdditionalAttributes
and stop settingRequired. - The version to pin in Task 1 is still whichever release first carries the fix.
Verified on the PR branch (still open, awaiting a maintainer): full
MudBlazor.UnitTestssuite green (5611 passed), and both
override tests fail if the ordering is inverted.Upstream is merged. MudBlazor/MudBlazor#13613 was approved and merged on 2026-08-17 as
ff10b3b
(MudInput: Let UserAttributes override computed ARIA attributes). The only edit the maintainer made
was deleting the explanatory razor comment.Still blocked, but only on a release. The latest MudBlazor release is v9.8.0 (2026-08-05), which
predates the merge, and this repo pins9.8.0inDirectory.Packages.props. Task 1 stays "raise the
pin" — to whatever version first shipsff10b3b.The scope of this issue just got wider, in a good way
Within a day of merging, the maintainer generalised the pattern across the library:
PR components #13641 MudCardMedia,MudImage,MudNavGroup,MudNavMenu#13642 MudFileUpload,MudRangeInput,MudMask,MudRadioGroup#13644 MudCheckBox,MudSwitch,MudRadio(aria-hidden on overridden labels)Every
aria-requiredemission in the library now uses the same ordering — fallback written first,
@attributes="UserAttributes"after it,requiredleft below the splat:aria-required="@Required.ToString().ToLowerInvariant()" @attributes="UserAttributes" ... required="@Required"
Two consequences for this plan:
- File upload can go ARIA-only too.
MudFileUploadnow honours a caller-suppliedaria-required,
so the marking added for Mark required file-upload fields — the one field type #199 left unannounced #262 can drop its HTML5 attribute in the same sweep rather than staying an
exception. - Select and autocomplete are still out of scope. Upstream emits no
aria-requiredon them at all
(MudSelecthas a caller-winsGetInputUserAttributes(), but nothing putsaria-requiredin it), so
that gap is unchanged and is not fixed by raising the pin.
Task 2 should therefore cover text, numeric, date and file upload, not the three originally listed.
- File upload can go ARIA-only too.
Unblocked — the gate is open. MudBlazor v9.9.0 shipped 2026-08-24 and contains
ff10b3b
(#13613), plus the follow-on sweep (#13641, #13642, #13644).Verified against the shipped NuGet binary, not the source —
ilspycmdon
MudBlazor 9.9.0/lib/net10.0/MudBlazor.dll:MudInput<T>.BuildRenderTree(both branches)AddAttribute(15, "aria-describedby", …) AddAttribute(38, …) AddAttribute(16, "aria-invalid", …) AddAttribute(39, …) AddAttribute(17, "aria-required", …) AddAttribute(40, …) ← fallbacks AddMultipleAttributes(18, UserAttributes) AddMultipleAttributes(41, …) ← caller wins … AddAttribute(31, "required", Required) AddAttribute(55, …) ← still after, still not overridableFor contrast, the same method on 9.8.0 — measured earlier in #199 — had the splat at 15/38 and
aria-requiredat 31/55, i.e. the reverse, which is exactly why #199 could not be done the right way.MudFileUpload<T>is the same shape (aria-required 15 → splat 16 → required 22), so file upload is
in scope for this issue now, as flagged in the previous comment.MudSelect<T>still emits zeroaria-requiredoccurrences, so select/autocomplete remain out of
scope and are not fixed by the version bump.State of the tasks
- Task 1 — raise the pin.
Directory.Packages.propsstill reads9.8.0; no Renovate PR has opened
yet (v9.9.0 is hours old).9.9.0is the version to pin. - Task 2 — flip to ARIA-only on both render paths, for text, numeric, date and file upload.
- Tasks 3–4 unchanged.
Nothing else stands in the way.
- Task 1 — raise the pin.
- added a commit that references this issue
on Aug 26, 2026
Problem / motivation
#199 asked for
aria-required="true"without the HTML5requiredattribute — the accessibilityannotation without the browser-validation attribute the project's convention forbids. That turned out to
be unreachable on MudBlazor 9.8.0, so #199 shipped the compromise (option A): drive
RequiredfromIsRequiredand accept the HTML5 attribute.Why it was unreachable. Decompiled from
MudBlazor.dll9.8.0 —MudInput<T>.BuildRenderTree:The caller's
UserAttributessplat lands before MudBlazor's own writes, and the Blazor render treeresolves duplicate attributes last-write-wins. So a caller can never override
aria-required, and onebooldrives both attributes together. Confirmed identical on 9.5.0, 9.7.0 and 9.8.0.Why this is fixable rather than a fact of life. MudBlazor 9.7/9.8 added a helper that does exactly
what is needed — but wired it only to the hidden-input presenter
<div>, not to the real input:And MudBlazor's own
AGENTS.md:213states the rule the ordinary render path violates:Proposed solution
Upstream first, then revert the compromise here.
A verified patch already exists at
/Users/philippe/repo/phmatray/public/mudblazor-aria-required.patch(76 insertions, 10 deletions). It extends MudBlazor's own pattern to the real
<input>/<textarea>:aria-required,aria-invalidandaria-describedbymove into a caller-wins dictionary(
GetInputUserAttributes()), whilerequireddeliberately stays bound to the parameter — which isprecisely what makes the pair separable.
Verified locally before filing:
dotnet build -c Release(net8.0 / net9.0 / net10.0)Once a MudBlazor release carries it, FormCraft flips to
Required="false"plusaria-required="true"throughUserAttributes, dropping the HTML5 attribute — which resolves theCLAUDE.mdconvention tension #199 had to amend rather than satisfy.Alternatives considered
form's
novalidate(novalidate is applied by script to the first form on the page, so the documented guarantee can miss #206). Rejected as the end state only because it required amending a documentedconvention to accommodate an upstream limitation, and because
requiredstill matches:required/:invalidin consumer CSS.required. Rejected: a JS dependency, invisible to bUnit, andfragile across re-renders — it would make the "no HTML5 attribute" claim untestable.
aria-label). This was optionB during Required fields are not identified to assistive technology on either render path #199's triage. Rejected then and now: a text convention rather than the standard programmatic
flag.
Area
FormCraft.ForMudBlazor — field rendering (both render paths), accessibility, upstream dependency
Follow-up from #199 (landed as #254). Related: #190, #204, #206, #203
🧠 Brainstorm
Problem / context
FormCraft's stated convention (
CLAUDE.md) is thatRequired()adds validation but not the HTML5requiredattribute, with browser validation disabled vianovalidate. #190 enforced that literally byremoving MudBlazor's
Requiredfrom the collection path. #199 discovered the cost — every required fieldwas announcing
aria-required="false", an affirmatively wrong statement to a screen reader — and had tochoose between two things the convention had accidentally welded together:
required— browser constraint validation. Unwanted by convention.aria-required="true"— accessibility annotation. Wanted, and lost as collateral.MudBlazor 9.8.0 offers no way to have (2) without (1). #199 chose (1)+(2) over neither, and amended the
convention to say what it always meant: it governs browser constraint validation, not ARIA.
Approaches
A. Fix upstream, then revert here. Extend MudBlazor's
GetDisplayUserAttributespattern to the realinput; once released, FormCraft sets
Required="false"+aria-required="true"viaUserAttributes.Pros: satisfies the original convention and WCAG; fixes it for every MudBlazor consumer; the patch is
small, idiomatic, and honours their own
AGENTS.md. Cons: blocks on an external maintainer.B. Accept option A as permanent. Pros: zero further work. Cons: leaves a documented convention
amended around a third-party limitation, and
requiredstill matches CSS selectors consumers may relyon.
C. Abstract behind a FormCraft-owned input wrapper. Pros: full control. Cons: reimplementing
MudInputto change two attributes; enormous surface, permanent maintenance.Recommendation
A, with B as the standing fallback (which is what ships today, so there is no accessibility risk
while waiting). The patch is written and verified; the remaining cost is the upstream round-trip. If the
PR is rejected or stalls indefinitely, close this as "won't do" and keep B — that is a legitimate
outcome, not a failure.
📋 Spec
Goal
A
.Required(...)field rendersaria-required="true"and no HTML5requiredattribute, on bothrender paths — the outcome #199 originally specified.
Scope
MudBlazorpin inDirectory.Packages.props.EffectiveNativeRequired's consumers so the ARIA annotation travels viaUserAttributeswhileMudBlazor's
Requiredparameter is leftfalsefor the inference case..WithNativeRequired()as the opt-in to MudBlazor's native semantics (asterisk + HTML5attribute) — that stays parameter-driven and is unaffected.
CLAUDE.md's convention wording and update the README release note.Non-goals
.WithNativeRequired()keeps it available deliberately.FormCraftcore, which stays UI-framework-agnostic.Behaviour
flowchart TD A["field.Required(\"…\")"] --> B{MudBlazor release<br/>honours caller ARIA?} B -->|"no — today"| C["Required=true<br/>aria-required=true + HTML5 required"] B -->|"yes — after upstream"| D["Required=false<br/>aria-required=true via UserAttributes"] D --> E["announced ✅<br/>no HTML5 attribute ✅<br/>no asterisk unless .WithNativeRequired()"]Requiredalso drops MudBlazor's asterisk forplain
.Required(...)fields. #199 shipped that asterisk and documented it as a visible WCAG 3.3.2identification, so removing it is a real regression unless FormCraft renders its own marker. Decide
this deliberately — it is the one part of this change that is not a pure win.
Key files
Directory.Packages.props— theMudBlazorversion pin.FormCraft.ForMudBlazor/Fields/MudBlazorFieldComponentBase.cs—EffectiveNativeRequired,NativeRequired.Resolve.FormCraft.ForMudBlazor/Features/CollectionField/CollectionFieldComponent.razor.cs—AddCommonFieldAttributes,RenderBooleanField.FormCraft.ForMudBlazor.UnitTests/Fields/AriaRequiredTests.cs,CollectionRequiredTests.cs,Components/RenderPipelineParityTests.cs.CLAUDE.md,README.md.Validation rules
.Required(...)field rendersaria-required="true"and norequiredattribute, both paths..WithNativeRequired()still rendersRequired="true"— asterisk and HTML5 attribute included..WithNativeRequired(false)on a required field still suppresses the annotation.RenderPipelineParityTestsstill comparesaria-requiredacross both paths and still bites.Edge cases
UserAttributesroute (Required fields are not identified to assistive technology on either render path #199), becauseMudCheckBoxemits noaria-requiredof its own. They need no change — and are the proof the mechanism works.Assumptions
(e.g. an opt-in parameter), this plan's Task 2+ adapts to whatever lands.
dev.🛠️ Implementation plan
Goal: land the upstream fix, then drop FormCraft's HTML5-attribute compromise.
Architecture: all FormCraft changes in
FormCraft.ForMudBlazor; core untouched. Both render pathschange together or
RenderPipelineParityTestsfails — that is intended.Tech stack: .NET 8 / 10 multi-target, Blazor, MudBlazor 9, xUnit + bUnit + Shouldly.
Global constraints:
dev; commit asPhilippe Matray <phmatray@gmail.com>; conventional commits.TreatWarningsAsErrors=true;dotnet test --filteris inert (MTP0001) — run the whole suite.NativeRequired.Resolve(...)implementation (Required fields are not identified to assistive technology on either render path #199)..WithNativeRequired()must keep meaning "MudBlazor's native decoration", asterisk included.Task 1: Open the upstream MudBlazor PR
Files: none in this repo. The patch is at
/Users/philippe/repo/phmatray/public/mudblazor-aria-required.patch.Interfaces:
MudInput.GetInputUserAttributes()— caller-wins fallbacks foraria-required,aria-invalid,aria-describedby.MudBlazor/MudBlazor, branch fromdev, apply the patch withgit apply.AGENTS.md:213and the existingGetDisplayUserAttributes()precedent, and stating the consumer case (server-side validation withnovalidate).Task 2: Raise the MudBlazor pin
Files: modify
Directory.Packages.props.Interfaces: none.
MudBlazorto the first release carrying the fix.dotnet build -c Releaseanddotnet test -c Release→ both green on the existing behaviour, proving the bump alone changes nothing.build(deps): raise MudBlazor to <version> for caller-overridable ARIA.Task 3: Flip both render paths to the ARIA-only mechanism
Files: modify
FormCraft.ForMudBlazor/Fields/MudBlazorFieldComponentBase.cs; modifyFormCraft.ForMudBlazor/Features/CollectionField/CollectionFieldComponent.razor.cs; modifyAriaRequiredTests.csandCollectionRequiredTests.cs.Interfaces: the inference contributes
aria-requiredviaUserAttributes; MudBlazor'sRequiredparameter is reserved for the explicit.WithNativeRequired()opt-in..Required(...)field rendersaria-required="true"and norequiredattribute, on both paths, for text, numeric, date and select.NativeRequired.Resolve(...)for the explicit opt-in drivingRequired, and route theIsRequiredinference into anaria-requireduser attribute on both paths..Required(...)fields — and pin the decision with a test.CollectionRequiredTestscases that assert the HTML5 attribute's presence (they were inverted the other way by Required fields are not identified to assistive technology on either render path #199).feat(mudblazor): announce required fields without the HTML5 required attribute.Task 4: Restore the convention wording and document
Files: modify
CLAUDE.md; modifyREADME.md; modifyFormCraft.ForMudBlazor/Extensions/FieldBuilderExtensions.cs(WithNativeRequiredXML doc).Interfaces: none new.
CLAUDE.md's validation convention to its literal form —Required()adds validation but not the HTML5 attribute — noting that ARIA annotation is separate and now independently achievable.## 🎉 Unreleasedbullet explaining that the HTML5 attribute is gone again, what it means for the asterisk, and that.WithNativeRequired()restores native semantics.WithNativeRequired's XML doc — the Level A warning onfalsestill applies, but the "why the HTML5 attribute comes back" paragraph becomes historical.dotnet build -c Releaseanddotnet test -c Release→ both green.docs: restore the validation convention now ARIA is independent.