Description
Supporting text below a control. A sibling, not a Field part. id is required so you can name it in aria-describedby. The component writes nothing onto the control.
Put aria-describedby on Field.Root, not on Field.TextInput. Root copies the string onto the input. TextInput then overwrites input-level aria.
With Field
Description owns its spacing. margin-block-start is field.label.gap (0.4rem) from the control. margin-block-end is 0. Put it after the control. Do not wrap it.
.labelled-field still spaces Label to the control. It does not space the control to Description.
.labelled-field {
display: flex;
flex-direction: column;
}
.labelled-field
> :not([data-todae-description], [data-todae-error])
+ :not([data-todae-description], [data-todae-error]) {
margin-block-start: var(--todae-field-label-gap);
}
const id = useId()
const hintId = useId()
<div className="labelled-field">
<Label htmlFor={id}>Work email</Label>
<Field.Root id={id} aria-describedby={hintId}>
<Field.TextInput value={v} onChange={(event) => setV(event.target.value)} />
</Field.Root>
<Description id={hintId}>We only use this for billing receipts.</Description>
</div>
This is not Dialog.Description. Dialog Description wires aria-describedby for you. Form Description does not.
When to use
When: Hint text below a control, as a sibling after it. Needs id; list it in aria-describedby on the control, or on Field.Root inside a Field.
When not: Validation errors → ErrorMessage. Dialog supporting text → Dialog.Description (wires itself). The visible name → Label.
Props
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| id | string | yes | — | Id the author lists in aria-describedby on the control or Field.Root. |
| children | React.ReactNode | no | — | Hint text. |
| className | string | no | — | Optional class on the span. |