Loading…
Loading…
Associates a text description with a form control for accessibility and clarity.
The Label component provides a textual descriptor that associates with a form control, establishing the fundamental connection between what a user sees and what a form element does. Labels are arguably the single most important accessibility element in any form — without them, screen reader users cannot identify form controls, and all users lose the click-target expansion that labels provide.
In HTML, the <label> element has a specific behavioral contract: clicking a label focuses or activates its associated control. This implicit interaction is built into the browser and requires no JavaScript — but it only works when the association is correctly established, either through the for/htmlFor attribute matching an id, or through nesting the control inside the <label>.
When to use a Label:
<fieldset> and <legend> insteadWhen NOT to use a Label:
<span>A label is not optional. Every form control needs a programmatic label — visible or visually hidden. The only exception is when aria-label or aria-labelledby provides the accessible name through other means. Use the Contrast Checker to ensure label text meets readability requirements.
| Variant | Purpose | Visual Treatment |
|---|---|---|
| Standard | Default label above a form control | Regular weight, standard font size, block display |
| Inline | Label beside a control (checkbox, radio, switch) | Regular weight, same line as control, pointer cursor |
| Floating | Animates from placeholder to top-label on focus | Smaller font when floated, transitions on focus/fill |
| Hidden | Visually hidden but accessible to screen readers | .sr-only / visuallyHidden CSS class |
| Legend | Labels a group of controls (fieldset) | Typically bold, may include group description |
| Element | Purpose | Required |
|---|---|---|
| Label Text | Primary descriptor ("Email address") | Yes |
| Required Indicator | Signals mandatory field (asterisk, text, or icon) | When field is required |
| Optional Indicator | "(optional)" suffix when most fields are required | When using optional-marking pattern |
| Helper Text | Additional guidance below the control ("Must be at least 8 characters") | No |
| Error Message | Validation feedback ("Please enter a valid email") | On validation failure |
| Character Counter | Shows current/max character count | For length-limited fields |
| Tooltip Trigger | Info icon with explanatory tooltip | For complex or ambiguous fields |
| Pattern | Implementation | Recommendation |
|---|---|---|
Asterisk (*) | Red asterisk after label text | Most common. Must include legend: "* = required" |
| "(required)" text | Text appended to label | Most explicit. Best for accessibility. |
| "(optional)" text | Text appended to non-required labels | Better when most fields are required |
| Bold label | Required labels are bold, optional are regular weight | Subtle. Not sufficient alone. |
| Property | Type | Default | Description |
|---|---|---|---|
htmlFor | string | — | The id of the associated form control. Required for proper association. |
required | boolean | false | Shows a required indicator (asterisk or text) |
requiredIndicator | 'asterisk' | 'text' | ReactNode | 'asterisk' | Style of the required indicator |
optionalIndicator | boolean | false | Shows "(optional)" text instead of required indicator |
disabled | boolean | false | Applies muted styling when the associated control is disabled |
error | boolean | false | Applies error styling (typically red text) |
size | 'sm' | 'md' | 'lg' | 'md' | Font size matching the associated control's size |
tooltip | string | ReactNode | — | Content for an info tooltip icon beside the label |
helperText | string | — | Guidance text rendered below the associated control |
errorMessage | string | — | Error message rendered below the control on validation failure |
children | ReactNode | — | The label text content |
Important: The htmlFor attribute (React's equivalent of HTML for) must exactly match the id on the form control. If they don't match, clicking the label won't focus the control, and screen readers won't associate them. This is the #1 label bug in production code.
| Token Category | Token Example | Label Usage |
|---|---|---|
| Color – Text | --color-text-primary | Default label text color |
| Color – Required | --color-error-500 | Required asterisk color |
| Color – Disabled | --color-text-disabled | Label text when associated control is disabled |
| Color – Error | --color-error-600 | Label text in error state |
| Color – Helper | --color-text-secondary | Helper text and optional indicator |
| Typography – Size | --font-size-sm (14px) | Default label font size |
| Typography – Weight | --font-weight-medium (500) | Label text weight — medium distinguishes from body text |
| Typography – Family | --font-family-sans | System sans-serif font. Preview with Font Explorer. |
| Spacing – Gap | --space-1 (4px) | Gap between label text and required indicator |
| Spacing – Margin | --space-1.5 (6px) | Bottom margin between label and its control |
| Spacing – Helper | --space-1 (4px) | Top margin between control and helper/error text |
Labels are deceptively token-light — most of their visual weight comes from typography tokens. The critical design decision is weight differentiation: labels should be visually distinct from body text (often medium weight vs regular) but not compete with headings (never bold/semibold unless it's a legend).
| State | Visual Treatment | Notes |
|---|---|---|
| Default | Standard text color, medium weight | The resting state alongside an unfocused control |
| Focused | May shift to brand color when associated control is focused | Optional. Some systems keep labels static during focus. |
| Filled | Remains default or shifts slightly to indicate completion | For floating labels: smaller font, translated to top position |
| Disabled | Muted/gray text color, no pointer interaction | Must visually match the disabled control |
| Error | Red/error color text, error message visible below control | Label itself may turn red, or only the error message does — choose one pattern and be consistent |
| Required | Asterisk or "(required)" visible | Always visible from initial render, not just after validation |
| Read-only | Same as default but associated control is non-editable | Label styling typically unchanged |
| State | Position | Font Size | Color |
|---|---|---|---|
| Empty + Unfocused | Inside input, vertically centered | Same as input placeholder | --color-text-tertiary |
| Empty + Focused | Translated above input, overlapping border | 0.75rem | --color-primary-600 |
| Filled + Unfocused | Translated above input | 0.75rem | --color-text-secondary |
| Filled + Focused | Translated above input | 0.75rem | --color-primary-600 |
| Error | Translated above input | 0.75rem | --color-error-600 |
Floating labels must transition smoothly. Use transform: translateY() and font-size transitions (150–200ms ease) rather than changing top/position to avoid layout thrash.
Labels are the foundational accessibility element for forms. Getting them wrong breaks the experience for screen reader users, voice control users, and users with motor impairments.
Programmatic Association (WCAG 1.3.1 – Info and Relationships):
<label for="id">, nesting the control inside <label>, or using aria-label/aria-labelledbyfor/htmlFor attribute must exactly match the control's id. Test this: click the label — if the control doesn't focus, the association is broken<fieldset> with <legend> instead of individual labels for the group titleVisible Labels (WCAG 2.5.3 – Label in Name):
aria-label="user email input field", voice control users saying "click Email" won't matcharia-label text — it creates a disconnect between visual and programmatic labelsTarget Size (WCAG 2.5.8 – Target Size Minimum):
Color Contrast (WCAG 1.4.3 – Contrast Minimum):
color-text-secondary) frequently fall just below 4.5:1 — test these explicitlyrequired attribute provides programmatic indication), some teams accept 3:1 for the asterisk aloneError Identification (WCAG 3.3.1 – Error Identification, 3.3.2 – Labels or Instructions):
aria-describedbyaria-describedby must exist in the DOMrequired or aria-required="true")Placeholder is NOT a Label (WCAG 1.3.1, 3.3.2):
Do:
<label> with htmlFor/for matching the control's id — this is the most reliable association methodaria-describedbyDon't:
aria-label when a visible label is feasible — visible labels benefit all users, not just screen reader users.sr-only if space is truly constrained<!-- Standard label above input -->
<div class="form-field">
<label for="email" class="label">
Email address
<span class="label-required" aria-hidden="true">*</span>
</label>
<input type="email" id="email" required aria-describedby="email-helper" />
<span id="email-helper" class="label-helper">We'll never share your email.</span>
</div>
<!-- Label with error state -->
<div class="form-field form-field--error">
<label for="password" class="label label--error">
Password
<span class="label-required" aria-hidden="true">*</span>
</label>
<input
type="password"
id="password"
required
aria-invalid="true"
aria-describedby="password-error"
/>
<span id="password-error" class="label-error" role="alert">
Password must be at least 8 characters.
</span>
</div>
<!-- Inline label for checkbox -->
<div class="form-field form-field--inline">
<input type="checkbox" id="terms" />
<label for="terms" class="label label--inline">
I agree to the Terms of Service
</label>
</div>
<!-- Visually hidden label -->
<div class="form-field">
<label for="search" class="sr-only">Search</label>
<input type="search" id="search" placeholder="Search…" />
</div>
<style>
.label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-primary);
margin-bottom: 6px;
}
.label-required {
color: var(--color-error-500);
margin-left: 2px;
}
.label-helper {
display: block;
font-size: 0.8125rem;
color: var(--color-text-secondary);
margin-top: 4px;
}
.label--error {
color: var(--color-error-600);
}
.label-error {
display: block;
font-size: 0.8125rem;
color: var(--color-error-600);
margin-top: 4px;
}
.label--inline {
display: inline;
font-weight: 400;
margin-bottom: 0;
cursor: pointer;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>import React from 'react';
import styles from './Label.module.css';
import clsx from 'clsx';
interface LabelProps {
htmlFor: string;
required?: boolean;
requiredIndicator?: 'asterisk' | 'text' | React.ReactNode;
optionalIndicator?: boolean;
disabled?: boolean;
error?: boolean;
size?: 'sm' | 'md' | 'lg';
tooltip?: string | React.ReactNode;
helperText?: string;
errorMessage?: string;
children: React.ReactNode;
}
export function Label({
htmlFor,
required = false,
requiredIndicator = 'asterisk',
optionalIndicator = false,
disabled = false,
error = false,
size = 'md',
tooltip,
helperText,
errorMessage,
children,
}: LabelProps) {
const helperId = helperText ? `${htmlFor}-helper` : undefined;
const errorId = errorMessage ? `${htmlFor}-error` : undefined;
return (
<div className={clsx(styles.field, { [styles.disabled]: disabled })}>
<label
htmlFor={htmlFor}
className={clsx(styles.label, styles[size], {
[styles.error]: error,
})}
>
{children}
{required && (
requiredIndicator === 'asterisk' ? (
<span className={styles.required} aria-hidden="true">*</span>
) : requiredIndicator === 'text' ? (
<span className={styles.requiredText}>(required)</span>
) : (
requiredIndicator
)
)}
{optionalIndicator && !required && (
<span className={styles.optional}>(optional)</span>
)}
{tooltip && (
<span className={styles.tooltip} title={typeof tooltip === 'string' ? tooltip : undefined}>
ℹ
</span>
)}
</label>
{/* The form control goes here (passed separately or via composition) */}
{errorMessage && (
<span id={errorId} className={styles.errorMessage} role="alert">
{errorMessage}
</span>
)}
{helperText && !errorMessage && (
<span id={helperId} className={styles.helperText}>
{helperText}
</span>
)}
</div>
);
}Material Design (MUI) integrates labels into form components via the label prop on <TextField>, <FormControlLabel> (for checkboxes, radios, switches), and <InputLabel> (standalone). MUI's floating label is the default behavior: the label animates from inside the input to above it on focus. The <FormHelperText> component handles helper and error messages. MUI automatically generates matching id and htmlFor associations. The shrink prop controls whether a floating label stays in its "floated" position.
Ant Design uses the <Form.Item label="..."> wrapper which renders a <label> with automatic htmlFor association to the nested control. The required prop adds a red asterisk. Validation messages appear via rules on <Form.Item>, with error text rendered below the control. Ant supports tooltip on Form.Item for inline help icons. The label positioning (labelCol/wrapperCol) uses Ant's grid system for horizontal form layouts.
Chakra UI provides <FormLabel> which renders a <label> with Chakra's typography styling. The <FormControl> wrapper manages isRequired, isInvalid, and isDisabled states, propagating them to <FormLabel>, <Input>, <FormHelperText>, and <FormErrorMessage>. Chakra's required indicator defaults to a red asterisk via the requiredIndicator prop, customizable to any ReactNode. All id and aria-describedby associations are generated automatically.
Bootstrap styles <label> via the .form-label class (margin-bottom: 0.5rem, inline block). Floating labels use the .form-floating wrapper: the label is placed after the input in source order and positioned via CSS. Bootstrap's .form-text class handles helper text. Required indicators, error messages, and validation styling are handled via Bootstrap's validation classes (.is-invalid, .invalid-feedback).
Apple Human Interface Guidelines emphasizes that every control needs a clear, concise label. In SwiftUI, labels are first-class: TextField("Email", text: $email) — the first parameter is the label. Toggles, pickers, and steppers all take label parameters. Apple recommends title case for labels and advises against using the label to repeat the data type ("Email" not "Email text field"). On iOS, labels are visually integrated into the control row in form lists.
Tailwind CSS relies on native <label> elements styled with utility classes: block text-sm font-medium text-gray-700 mb-1 for standard labels, sr-only for visually hidden labels. There is no Label component — Tailwind is utility-first. Required asterisks: <span class="text-red-500">*</span>. Error messages: <p class="text-sm text-red-600 mt-1">. The Headless UI and Radix UI libraries (commonly used with Tailwind) provide <Label> primitives with automatic association via context.