Field

A label, a hint and an error, bound to one control.

import { Field } from "@dede-style/ui/field", then wrap a control in it: <Field label="Email address"><TextInput …/></Field>. It knows nothing about text inputs — a Select, a Textarea and a Checkbox all need the same four rows and the same wiring, and only the box in the middle changes.

We'll only use this to reply.

Click the words and the caret lands in the box. That is the whole of what this component buys, and it is worth more than it sounds: a real <label> bound by htmlFor gives every field a larger target, an announced name, and a name that survives the control being swapped for a different one.

The hint and the error reach the control the same way, through aria-describedby. All three ids come from one useId inside Field, so there is nothing to type and nothing to mistype.

The hint goes above the control

Instructions are read before you type, not after.

16 digits, on the front of the card.

Most systems put the hint under the box, and it is the wrong place twice over. A hint is instructions for filling the field in — below the control it is routinely missed, and on a phone the keyboard covers it at the exact moment it is needed.

It also leaves the error nowhere clean to go. With the hint above, the error is the only row that appears and disappears, so nothing above the control ever moves — a form that reflows while you are correcting it is a form that loses your place.

The message is the state

There is no separate flag, so the two cannot disagree.

This one is live. Click into the box below and tab out of it with the address unfinished: the message appears as you leave, not while you type. Then finish the address and it goes on the next keystroke. With a screen reader on you will hear it arrive, which is what role="alert" buys and what a message already sitting on the page cannot do.

We will only use this to reply.

Passing an error string does four things at once: the message appears, the control’s border turns, aria-invalid arrives on the input and the message’s id joins aria-describedby. There is no invalid prop to set as well, because an error state with nothing to read is not a state, it is a colour.

Field never validates. Deciding when a field is wrong belongs to the form — the researched rule is to validate on blur and then re-validate on every keystroke once a message is already showing, which stops a half-typed address being called wrong and lets a correction clear itself. That is guidance for whoever owns the form, not behaviour to build in here: a component that decided for itself would have to know what valid means.

The message is red, and it is not only red. The words are the signal; the colour and the mark beside them are reinforcement, which is what keeps the field readable to somebody who cannot tell the border from a grey one.

A hidden label is still a label

Off the screen, in the accessibility tree.

labelHidden clips the words rather than removing them — display: none would take them out of the accessibility tree too, which is the precise opposite of the point. Use it where the control’s purpose is already obvious from what is beside it: a search box next to a search button.

It is never a way to save space. The placeholder is not standing in for the label, because it disappears the moment anything is typed — so a reader who is interrupted comes back to a filled box with nothing saying what it holds.

Optional is a word, not a mark

There is no asterisk, and that is a decision rather than an omission.

Three mechanisms were weighed. Marking required with an asterisk is the convention, and in a typical form it puts a mark on nearly every row — a mark that is almost always present carries no information. Marking optional automatically is better reading and worse engineering: it forces required to default to true, inverting the HTML default so that a control with no props written on it silently acquires browser validation.

The third removes the decision from the API. required is the attribute and nothing else, and an optional field says so in its own label. It needs no asterisk styling and no aria-hidden mark, it is silent by default everywhere, and it makes the marker words, which is where words belong. It is also what GOV.UK actually recommends.