Skip to content

Label

Associates a text description with a form control for accessibility and clarity.

  • Form Label
  • Field Label

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:

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

VariantPurposeVisual Treatment
StandardDefault label above a form controlRegular weight, standard font size, block display
InlineLabel beside a control (checkbox, radio, switch)Regular weight, same line as control, pointer cursor
FloatingAnimates from placeholder to top-label on focusSmaller font when floated, transitions on focus/fill
HiddenVisually hidden but accessible to screen readers.sr-only / visuallyHidden CSS class
LegendLabels a group of controls (fieldset)Typically bold, may include group description

Label Anatomy

ElementPurposeRequired
Label TextPrimary descriptor ("Email address")Yes
Required IndicatorSignals mandatory field (asterisk, text, or icon)When field is required
Optional Indicator"(optional)" suffix when most fields are requiredWhen using optional-marking pattern
Helper TextAdditional guidance below the control ("Must be at least 8 characters")No
Error MessageValidation feedback ("Please enter a valid email")On validation failure
Character CounterShows current/max character countFor length-limited fields
Tooltip TriggerInfo icon with explanatory tooltipFor complex or ambiguous fields

Required Field Patterns

PatternImplementationRecommendation
Asterisk (*)Red asterisk after label textMost common. Must include legend: "* = required"
"(required)" textText appended to labelMost explicit. Best for accessibility.
"(optional)" textText appended to non-required labelsBetter when most fields are required
Bold labelRequired labels are bold, optional are regular weightSubtle. Not sufficient alone.

Properties

Label Properties

PropertyTypeDefaultDescription
htmlForstring—The id of the associated form control. Required for proper association.
requiredbooleanfalseShows a required indicator (asterisk or text)
requiredIndicator'asterisk' | 'text' | ReactNode'asterisk'Style of the required indicator
optionalIndicatorbooleanfalseShows "(optional)" text instead of required indicator
disabledbooleanfalseApplies muted styling when the associated control is disabled
errorbooleanfalseApplies error styling (typically red text)
size'sm' | 'md' | 'lg''md'Font size matching the associated control's size
tooltipstring | ReactNode—Content for an info tooltip icon beside the label
helperTextstring—Guidance text rendered below the associated control
errorMessagestring—Error message rendered below the control on validation failure
childrenReactNode—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 CategoryToken ExampleLabel Usage
Color – Text--color-text-primaryDefault label text color
Color – Required--color-error-500Required asterisk color
Color – Disabled--color-text-disabledLabel text when associated control is disabled
Color – Error--color-error-600Label text in error state
Color – Helper--color-text-secondaryHelper 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-sansSystem 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

StateVisual TreatmentNotes
DefaultStandard text color, medium weightThe resting state alongside an unfocused control
FocusedMay shift to brand color when associated control is focusedOptional. Some systems keep labels static during focus.
FilledRemains default or shifts slightly to indicate completionFor floating labels: smaller font, translated to top position
DisabledMuted/gray text color, no pointer interactionMust visually match the disabled control
ErrorRed/error color text, error message visible below controlLabel itself may turn red, or only the error message does — choose one pattern and be consistent
RequiredAsterisk or "(required)" visibleAlways visible from initial render, not just after validation
Read-onlySame as default but associated control is non-editableLabel styling typically unchanged

Floating Label States (Detailed)

StatePositionFont SizeColor
Empty + UnfocusedInside input, vertically centeredSame as input placeholder--color-text-tertiary
Empty + FocusedTranslated above input, overlapping border0.75rem--color-primary-600
Filled + UnfocusedTranslated above input0.75rem--color-text-secondary
Filled + FocusedTranslated above input0.75rem--color-primary-600
ErrorTranslated above input0.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 using aria-label/aria-labelledby
  • The for/htmlFor attribute must exactly match the control's id. 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-label text — 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 required attribute 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-describedby must 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 (required or aria-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> with htmlFor/for matching the control's id — 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-label when 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-only if space is truly constrained

Code Snippets

html
<!-- 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>
tsx
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.

TypographyForm LabelField Label