Skip to content

Helper text and error messages are TextField-only; other fields cannot explain a validation failure #41

Description

@microbit-matt-hillsdon

Label, helper text and error message only exist inside TextField. Any other
control that needs to explain a validation failure has no way to do it, and no
similar code to copy — you have to rebuild the field wrapper by hand.

This looks like the extraction fitting the apps' current usage: TextField was
the only control the apps validated with a message, so the field concerns were
built into it rather than beside it.

Where things stand

Component invalid styling helper text error message
TextField yes (input recipe) yes yes (RAC FieldError)
Input (bare) yes
NativeSelect yes (input recipe)
NumberField yes (renders input() inside)
Select / ComboBox yes, once #38 lands
Checkbox no
Radio / RadioGroup no
Switch no
Slider n/a — RAC has no validation

errorMessage and helperText appear only in TextField.recipe.ts and
TextField.tsx. Nothing else has the slots or the props.

Two consequences worth separating:

  1. No control other than TextField can carry an error message. A Select can
    go red, but nothing can say why. Colour as the only signal fails WCAG 1.4.1
    and 3.3.1. An app cannot work around it: children go to the listbox, and
    RAC's FieldError has to be rendered inside the RAC field component to be
    wired up with aria-describedby.
  2. Checkbox, Radio/RadioGroup and Switch have no invalid styling at all. RAC
    sets data-invalid on all three; none of our recipes read it. A required
    checkbox in a submitted form changes nothing visually.

Options

A — per-component errorMessage / helperText props, mirroring TextField.
Small and predictable, and each component keeps control of where the message
sits. But it is the same four slots re-implemented per component (Select,
NumberField, NativeSelect, Checkbox, Radio, Switch), and every new control pays
the cost again.

B — a Field wrapper owning label, required indicator, helper text and error
message,
with TextField reduced to Field + Input. One implementation, and
it matches what Chakra's FormControl did, so app code ports across. The catch
is that RAC's FieldError and the aria-describedby wiring live inside each
RAC field component, so the wrapper cannot render the message from outside —
each control still has to render a slot the wrapper fills, probably via context.

B is the better shape if we expect more than one or two more controls to need
messages. A is the cheaper way to unblock a specific app.

Either way, the missing invalid styling on Checkbox/Radio/Switch is independent
and can be fixed first: those recipes just need to read data-invalid.

Context

#38 fixes the related case where the invalid rule on the Select trigger could
never match, because RAC puts data-invalid on the field root rather than the
control. Worth reading that PR's comments before starting either option here —
the same "which element does RAC mark" question comes up for every control.


Drafted by Claude in a Claude Code session, from a survey of the library's
recipes and components. The findings were checked against the source in the
session; the wording has not had a separate review.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions