Overview
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:
- For every visible form control: Text Input, Select, Checkbox, Radio Button, Slider, Switch, File Upload
- For groups of related controls using
<fieldset>and<legend>instead - Anywhere a user needs to understand what data to provide
When NOT to use a Label:
- For buttons — buttons have their own text content
- For decorative descriptions — use a Text or
<span> - For search inputs with a visible search icon and placeholder — even here, add a visually hidden label
- As a general-purpose text element — Label has specific semantic meaning tied to form controls
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.
Variants
Common Label Variants
| 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 |
Label Anatomy
| 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 |
Required Field Patterns
| 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. |
Properties
Label Properties
| 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 Mappings
Design Token Mappings
| 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).
States
Label States
| 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 |
Floating Label States (Detailed)
| 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.
Accessibility
Accessibility
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):
- Every form control MUST have a programmatic label — either via
<label for="id">, nesting the control inside<label>, or usingaria-label/aria-labelledby - The
for/htmlForattribute must exactly match the control'sid. Test this: click the label — if the control doesn't focus, the association is broken - For groups of related controls (radio buttons, checkbox groups), use
<fieldset>with<legend>instead of individual labels for the group title
Visible Labels (WCAG 2.5.3 – Label in Name):
- The accessible name of a control must contain the visible label text. If the label says "Email" but
aria-label="user email input field", voice control users saying "click Email" won't match - Don't override visible labels with different
aria-labeltext — it creates a disconnect between visual and programmatic labels
Target Size (WCAG 2.5.8 – Target Size Minimum):
- Labels expand the click target of their associated control. This is especially valuable for small controls like checkboxes and radio buttons — the label makes the entire text area clickable
- Ensure the label + control combined target is at least 24×24 CSS pixels (WCAG 2.5.8 Level AA)
Color Contrast (WCAG 1.4.3 – Contrast Minimum):
- Label text must achieve 4.5:1 against its background. Use the Contrast Checker.
- Helper text and optional indicators (often
color-text-secondary) frequently fall just below 4.5:1 — test these explicitly - Required asterisks in red must meet 4.5:1, but since they're supplementary (the
requiredattribute provides programmatic indication), some teams accept 3:1 for the asterisk alone
Error Identification (WCAG 3.3.1 – Error Identification, 3.3.2 – Labels or Instructions):
- Error messages must be programmatically associated with their control via
aria-describedby - The error message ID referenced by
aria-describedbymust exist in the DOM - Don't rely solely on color to indicate errors — add text, icons, or border changes
- Required fields should be indicated both visually (asterisk or text) and programmatically (
requiredoraria-required="true")
Placeholder is NOT a Label (WCAG 1.3.1, 3.3.2):
- Placeholder text disappears on input and is not reliably read by all screen readers — it cannot substitute for a label
- Floating labels that start as placeholders must transition to a visible, persistent label state once the field has value — the label must remain visible at all times after activation
Usage Guidelines
Usage Guidelines
Do:
- Provide a visible label for every form control — no exceptions
- Use
<label>withhtmlFor/formatching the control'sid— this is the most reliable association method - Place labels above inputs for forms (fastest scan pattern per UX research) or inline for checkboxes/radios
- Use clear, concise label text: "Email address" not "Please enter your electronic mail address below"
- Indicate required fields consistently — pick one pattern (asterisk, text) and use it throughout the application
- Include a legend at the top of forms explaining your required indicator: "Fields marked with * are required"
- Associate error messages with controls using
aria-describedby
Don't:
- Don't use placeholder text as a label replacement — it disappears and is unreliable for accessibility
- Don't use
aria-labelwhen a visible label is feasible — visible labels benefit all users, not just screen reader users - Don't change label text dynamically based on input value — it confuses screen readers that cache the accessible name
- Don't put interactive elements (links, buttons) inside labels — clicking them may trigger the associated control
- Don't use ALL CAPS for labels — it reduces readability by 13–18% (Miles Tinker's legibility research) and screen readers may spell out each letter
- Don't remove labels to save space — visually hide them with
.sr-onlyif space is truly constrained
Code Snippets
<!-- 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>
);
}Design Systems
Design System Implementations
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.