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>
We only use this for billing receipts.

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

Description props
NameTypeRequiredDefaultDescription
idstringyes—Id the author lists in aria-describedby on the control or Field.Root.
childrenReact.ReactNodeno—Hint text.
classNamestringno—Optional class on the span.