Labelling components
Every interactive control needs a clear accessible name. The best option is always a visible label, connected to the control with Field or Label.
Visible labels: Field and Label
A visible label tells everyone what the control is for: mouse, keyboard, screen reader and voice input users alike.
<Field label="Full name" required>
<Input autoComplete="name" />
</Field>
<Checkbox label="Get the monthly newsletter" />
<Switch label="Battery saver" />
<Slider label="Brightness" showValue defaultValue={[60]} />
{/* Custom layout: Label + htmlFor pointing to the control's id */}
<Label htmlFor="bio" size="large">About you</Label>
<Textarea id="bio" rows={3} />
{/* Label with an explanation */}
<InfoLabel htmlFor="vat" info="The 10- or 13-digit tax ID issued by the tax authority.">Tax ID</InfoLabel>
<Input id="vat" inputMode="numeric" />Checkbox, Switch, Radio and Slider have their own label prop and connect it with useId. Wrap Input / Textarea / Select / Dropdown / Combobox in Field: it generates the id and sets htmlFor, aria-describedby and aria-invalid.
Icon-only buttons
An icon has no name. Every icon-only button needs an aria-label; add a Tooltip with the same text so sighted users can read it too.
<div role="toolbar" aria-label="Text formatting" className="flex gap-1">
<SimpleTooltip content="Bold (Ctrl+B)">
<Button appearance="subtle" icon={<TextBold20Regular />} aria-label="Bold" aria-keyshortcuts="Control+B" />
</SimpleTooltip>
<SimpleTooltip content="Italic (Ctrl+I)">
<Button appearance="subtle" icon={<TextItalic20Regular />} aria-label="Italic" aria-keyshortcuts="Control+I" />
</SimpleTooltip>
<SimpleTooltip content="Underline (Ctrl+U)">
<Button appearance="subtle" icon={<TextUnderline20Regular />} aria-label="Underline" aria-keyshortcuts="Control+U" />
</SimpleTooltip>
</div>
{/* Give the label enough context when the page has many identical buttons */}
<Button appearance="subtle" icon={<Delete20Regular />} aria-label="Delete file Q3 report.pdf" />- A Tooltip does not replace the label. It only shows on hover / focus, and Radix attaches it as a description (
aria-describedby). The name still has to come fromaria-label. - The label should match the tooltip text (WCAG 2.5.3, Label in Name) so voice control users can say the right name.
- When a page has many buttons with the same icon (Delete on every row), add context to the label: “Delete file Q3 report.pdf”.
Descriptions & validation: aria-describedby
The name says what the control is; the description adds what the user needs to know. Field connects both the hint and the validationMessage to the control.
<Field
label="Work email"
hint="Used to sign in and receive invoices."
validationState="error"
validationMessage="The email needs an @ and a domain, for example ann@company.com."
required
>
<Input type="email" defaultValue="ann.company.com" />
</Field><!-- HTML generated by Field -->
<label for=":r5:">Work email<span>*</span></label>
<input
id=":r5:"
required
aria-invalid="true"
aria-describedby=":r5:-message :r5:-hint"
/>
<span id=":r5:-message">The email needs an @ and a domain, for example ann@company.com.</span>
<span id=":r5:-hint">Used to sign in and receive invoices.</span>A screen reader reads: “Work email, required, invalid entry, edit text, The email needs an @ and a domain…, Used to sign in…”. The validationMessage is not read out when it appears. When the user submits a form with errors, move focus to the first invalid field; its label and error description are then read together.
Labels for groups: RadioGroup & Checkbox groups
Each option has its own label, but the group needs a name too. Otherwise a screen reader user hears “Standard” without knowing what they are choosing.
{/* RadioGroup: role="radiogroup" takes its name from a visible label through aria-labelledby */}
<div className="flex flex-col gap-1">
<Label id="shipping-label" weight="semibold">Shipping method</Label>
<RadioGroup aria-labelledby="shipping-label" defaultValue="standard">
<Radio value="standard" label="Standard (3–5 days)" />
<Radio value="express" label="Express (same day)" />
</RadioGroup>
</div>
{/* Checkbox group: fieldset + legend is the native way, no ARIA needed */}
<fieldset>
<legend className="pb-1 text-body1 font-semibold">Notify me by</legend>
<div className="flex flex-col">
<Checkbox label="Email" defaultChecked />
<Checkbox label="SMS" />
<Checkbox label="Mobile app" />
</div>
</fieldset>Note: the Field's <label for> can only name real form elements (input, select, textarea, button). If you put a RadioGroup inside a Field with a render-prop, the group still needs aria-labelledby pointing to a label with an id, as in the example above.
A placeholder is not a label
A placeholder disappears once you start typing, has low contrast (foreground-4) and is not read as the name by every screen reader.
One typed character removes the context; the expected format is unknown.
The label is always visible; the format is in the hint; the placeholder is only an example.
- Give every control a visible label: short, specific, sentence case.
- Use aria-label for icon-only buttons, with a Tooltip showing the same text.
- Name groups (RadioGroup, Checkbox groups) with aria-labelledby or fieldset + legend.
- Put formats and constraints in the hint so they are connected through aria-describedby.
- Using a placeholder instead of a label.
- Repeating the control type in the label (“Save button”, “Name input”). Screen readers already announce the role.
- Setting an aria-label that differs from the visible text.
- Relying on a Tooltip as the only name.