📝 Form validation & labels

Common mistake

A common mistake is relying solely on colour to indicate errors (e.g. a red border) without any text explanation. Another frequent issue is omitting labels on input fields entirely, or placing instructions only in placeholder text that disappears once the user starts typing. Screen reader users may never learn what a field expects, and all users are left guessing what went wrong when validation fails.

🎯 Relevant elements

This guideline applies to any widget that accepts user input or displays validation feedback:

  • Text fields (TextField, TextFormField)
  • Dropdowns and pickers (DropdownButton, DropdownMenu)
  • Checkboxes, radio buttons, and switches
  • Date and time pickers
  • Custom form controls built with GestureDetector or InkWell

WCAG Guideline

This guideline covers three related criteria:


Solution

Labels and instructions (3.3.2)

Every input field must have a persistent, visible label. Do not rely on hintText alone. It disappears when the user starts typing and is not consistently announced by screen readers.

// ❌ Don't - placeholder disappears, no persistent label
TextField(
  decoration: InputDecoration(
    hintText: 'Enter your email',
  ),
);

// ✅ Do - persistent label that remains visible and is announced
TextField(
  decoration: InputDecoration(
    labelText: 'Email address',
    hintText: 'e.g. name@example.com',
  ),
);

Error identification (3.3.1)

When validation fails, the error must be described in text via errorText. Flutter’s InputDecoration.errorText automatically associates the message with the field in the semantic tree.

// ❌ Don't - only a red border, no text explanation
TextField(
  decoration: InputDecoration(
    labelText: 'Email address',
    enabledBorder: OutlineInputBorder(
      borderSide: BorderSide(color: Colors.red),
    ),
  ),
);

// ✅ Do - error message is visible and announced by screen readers
TextField(
  decoration: InputDecoration(
    labelText: 'Email address',
    errorText: hasError ? 'Please enter a valid email address' : null,
  ),
);

Error suggestions (3.3.3)

When possible, tell the user how to fix the error, not just that it is wrong.

// ❌ Don't - vague error, user does not know what to fix
TextField(
  decoration: InputDecoration(
    labelText: 'Date of birth',
    errorText: hasError ? 'Invalid date' : null,
  ),
);

// ✅ Do - actionable suggestion
TextField(
  decoration: InputDecoration(
    labelText: 'Date of birth',
    errorText: hasError ? 'Enter a date in the format DD/MM/YYYY' : null,
  ),
);

Combining labels, errors, and helper text

TextFormField(
  decoration: InputDecoration(
    labelText: 'Password',
    helperText: 'Must be at least 8 characters',
    errorText: hasError ? 'Password is too short — use at least 8 characters' : null,
  ),
  obscureText: true,
  validator: (value) {
    if (value == null || value.length < 8) {
      return 'Password is too short — use at least 8 characters';
    }
    return null;
  },
);

Validation & Testing

Verify this guideline using one or more of the methods below. See the Validation & Testing setup guide for tool configuration.

SemanticsDebugger

Confirm that each input field shows its label and, when in error state, the error message in the semantic overlay.

accessibility_tools

Activate screen reader mode and confirm form field labels and error messages are visible in the overlay.

TalkBack & VoiceOver

Navigate through form fields and verify that:

  1. Each field announces its label when focused.
  2. Error messages are announced immediately after validation fails.
  3. The error message describes what is wrong and how to fix it.

flutter_test

Use flutter_test to assert that labels and error messages are present in the semantic tree.

testWidgets('email field has label and shows error', (tester) async {
  await tester.pumpWidget(MyFormWidget());

  // Verify label is present
  expect(find.text('Email address'), findsOneWidget);

  // Trigger validation
  await tester.tap(find.text('Submit'));
  await tester.pumpAndSettle();

  // Verify error message is shown
  expect(find.text('Please enter a valid email address'), findsOneWidget);
});

🛠️ Usage baseflow-a11y-components library

🛠️ Work in progress…


This site uses Just the Docs, a documentation theme for Jekyll.