Status
Accepted
Date
September 9, 2026
Context
Nothing in the system said how a required field should be marked. content/a11y/forms.md
documented the accessibility mechanics correctly but framed the visible indicator as a
conditional — "If using a visual asterisk (*)…", followed by "Alternatively, include
text '(required)' as sr-only". content/rules/accessibility.md required some visible
indicator without naming one. field.md and form.md said nothing at all. Two teams reading
the same corpus could therefore ship two different conventions and both be following the docs.
The two conventions in circulation are:
- Optional-by-default — required fields carry a visible
*, and the form carries a note explaining what the*means. - Required-by-default — every field is assumed required, and the few that aren't have labels reading "Label (optional)".
Each is internally coherent. Each is also destroyed by the presence of the other, which is what makes this worth a decision record rather than a style preference.
An unmarked label is the most common thing on any form, so whatever an unmarked label means is the convention's real payload. Under convention 1 an unmarked field means optional. Under convention 2 an unmarked field means required. Mix the two on one form — or across two screens of one product — and an unmarked field means nothing: the person cannot infer anything from the absence of a marker, so they have to read every label on the form before they can trust any of them. That is the whole cost. Two conventions here are not merely inconsistent with each other; each one is individually useless once the other is present, because each depends on the reader being able to draw a conclusion from silence.
This bites hardest in clinical intake, where forms are long, mostly-mandatory, and filled in by someone who is already unwell. It bites hardest of all for a screen reader user, who meets the labels one at a time and cannot scan the form for a marker to calibrate against.
Decision
Required fields carry a visible asterisk, and the form carries a note reading "Fields marked with an asterisk are required."
- The asterisk is rendered by the label,
aria-hidden="true", so it is not announced as a character. The control carriesaria-required(or the nativerequired), which is what actually tells assistive technology the field is mandatory. - The note is the key to the marker. An asterisk with no legend is decoration; the note is what makes it mean something to a first-time reader.
- When every field on the form is required, mark none of them. An asterisk on every label is
noise that distinguishes nothing. Say "All fields are required" once, above the fields, and
leave the labels clean.
aria-requiredstill goes on every control — the refinement is about the visible marker, not the semantics. - Labels reading "Label (optional)" are the rejected convention and must not appear.
API
| Piece | What it does |
|---|---|
required on FormItem | Renders the aria-hidden asterisk via FormLabel and sets aria-required on the control |
required on Field | Same, via FieldLabel, for the non-react-hook-form layout path |
<RequiredFieldsNote /> | The note. variant="marked" renders "Fields marked with an asterisk are required."; variant="all" renders "All fields are required." |
RequiredFieldsNote is self-hiding: it renders nothing when there is nothing to explain, so a
form that turns out to have no required fields does not display a legend for a marker that never
appears.
The prop is on the field wrapper rather than on the label or the control because both the marker
and aria-required have to come from one decision. Putting the asterisk on FormLabel and the
aria-required on Input separately is exactly how the two halves drift apart — a label with an
asterisk and a control with no aria-required is a field that looks required to a sighted reader
and optional to a screen reader.
Anti-pattern
// ✗ Required-by-default with "(optional)" labels
<FormItem>
<FormLabel>Middle name (optional)</FormLabel>
<FormControl><Input {...field} /></FormControl>
</FormItem>
// ✓ Optional-by-default with a marked required field
<RequiredFieldsNote variant="marked" />
<FormItem required>
<FormLabel>Last name</FormLabel>
<FormControl><Input {...field} /></FormControl>
</FormItem>
<FormItem>
<FormLabel>Middle name</FormLabel>
<FormControl><Input {...field} /></FormControl>
</FormItem>
What the dev warnings catch
Neither failure is visible to the type system — both compile, and both look fine on the screen
of whoever wrote them — so they are reported at runtime through lib/dev-warn.ts, dev-only and
once per page load:
- A label whose text ends in "(optional)" or "(required)", or that contains a literal
*character. All three are hand-rolled substitutes for therequiredprop, and the first is the rejected convention arriving one field at a time. - Required fields on a form with no
RequiredFieldsNote. This is the common accidental failure: the asterisks render, the form looks marked up correctly, and nothing on screen says what the asterisks mean.
Alternatives Considered
- Required-by-default with "(optional)" labels. Rejected. It is the better convention for a form where nearly everything is mandatory — fewer markers, less visual noise — and clinical intake forms are mostly that shape. It loses on two counts. First, requiredness is the consequential fact: a missed required field blocks the submit, whereas a missed optional field costs nothing, so the marker should be on the fields where being wrong hurts. Second, "(optional)" lives in the label text, which means it is translated, wrapped, and edited by whoever is writing copy — it degrades into "(Optional)", "- optional", "if known", each of which reads as a different rule. An asterisk emitted by a prop cannot drift that way.
- Ship both and let the surface choose, as
EditableSectiondoes withcard-editandform-save. Rejected, and the contrast is instructive: those two save models coexist because a person meets one surface at a time and each model is self-explanatory on its own. Required-field marking is the opposite — its meaning is carried by what isn't marked, so it is a system-wide convention or it is nothing. There is no unit small enough to scope it to. sr-only"(required)" text in the label instead of an asterisk. Rejected as a substitute, kept as an implementation detail. It only reaches screen reader users, so on its own it leaves sighted users with no indicator at all — and it stutters againstaria-required: "Name, required, required, edit text".- No visible marker; rely on
aria-requiredplus validation on submit. Rejected. It tells people which fields were mandatory only after they have failed to fill them, which is the pattern the forms guidance exists to prevent.
Consequences
content/a11y/forms.mdno longer offers the asterisk as one option among several. It keeps every accessibility mechanic it documented —aria-required,aria-hiddenon the marker, the announcement examples, the testing checklist — and points here for the convention itself.content/rules/accessibility.mdnames the convention rather than requiring an unspecified "visual indicator".field.mdandform.mdeach gained a Required fields section covering the API and the anti-pattern, pointing here rather than restating the reasoning.- Existing forms with "(optional)" labels will start logging a dev warning. They keep working; the warning is the migration signal.
- This closes a gap the UX corpus had recorded as open —
helix-ux§8.2 previously said the system had no position on marking optional fields and instructed reviewers to ask. It has one now.
When to Revisit
- If a product surface appears where nearly every field is optional — the inverse of clinical intake — the asterisk load may invert the trade-off. Note that the all-required refinement has no mirror image: "All fields are optional" is not a useful thing to tell someone.
- If a form is long enough that one note at the top is out of view by the time the person reaches
the fields, the fix is per-section notes, not a second convention. Revisit whether
RequiredFieldsNoteneeds a scoping mechanism, not whether the marker should change. - If localisation testing shows the asterisk carries a different or conflicting meaning in a supported locale, revisit the marker glyph — but keep the marked-required direction.