📝 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
GestureDetectororInkWell
WCAG Guideline
This guideline covers three related criteria:
- WCAG 2.2 — 3.3.1 Error Identification (Level A). If an input error is automatically detected, the item in error must be identified and the error described to the user in text.
- WCAG 2.2 — 3.3.2 Labels or Instructions (Level A). Labels or instructions must be provided when content requires user input.
- WCAG 2.2 — 3.3.3 Error Suggestion (Level AA). If an input error is automatically detected and suggestions for correction are known, the suggestions must be provided to the user, unless it would jeopardise security or purpose.
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:
- Each field announces its label when focused.
- Error messages are announced immediately after validation fails.
- 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…