Form Validation
Core Forms provides real-time inline validation with accessible error messaging. Validation runs on the client side for immediate feedback and on the server side for security. Multi-step forms validate per step before allowing the user to advance.
Inline Validation on Blur
Fields are validated when the user leaves them (blur event). This gives immediate feedback without interrupting typing:
// Core Forms validates on blur for each field
field.addEventListener('blur', () => validateField(field));
On form submission, all fields are validated together. In multi-step forms, validation runs on the current step's fields when the user clicks Next.
Validation Rules
Required Fields
Fields with the required attribute must have a non-empty value. Core Forms handles different input types correctly:
<!-- Text input: value must not be empty or whitespace-only -->
<input type="text" name="name" required>
<!-- Select: must have a value (not the empty placeholder) -->
<select name="country" required>
<option value="">Select a country</option>
<option value="US">United States</option>
</select>
<!-- Checkboxes: at least one must be checked -->
<input type="checkbox" name="terms" value="yes" required>
<!-- Radio buttons: one must be selected -->
<input type="radio" name="plan" value="free" required>
<input type="radio" name="plan" value="pro">
Checkbox and radio validation: Core Forms checks the .checked property rather than .value. For checkbox groups sharing the same name, at least one must be checked.
The "0" Value Fix
A common bug in form validation is treating the string "0" as empty. Core Forms explicitly handles this -- a required field with a value of "0" passes validation:
// Core Forms required check (simplified)
function isFieldEmpty(field) {
const value = field.value.trim();
// "0" is a valid value for required fields
if (value === '0') return false;
return value === '';
}
This matters for fields like quantity selectors, rating scales, or any numeric input where zero is a legitimate answer.
Email Format Validation
Fields with type="email" are validated against a standard email pattern:
<input type="email" name="email" required>
The validation checks for:
- At least one character before @
- A domain with at least one dot
- At least two characters in the TLD
// Email pattern used by Core Forms
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
HTML5 Constraint Validation
Core Forms respects all standard HTML5 validation attributes:
| Attribute | Applies To | Description |
|---|---|---|
min |
number, date, range |
Minimum allowed value |
max |
number, date, range |
Maximum allowed value |
minlength |
text, textarea, password |
Minimum character count |
maxlength |
text, textarea, password |
Maximum character count |
pattern |
text, email, tel, url |
Regex pattern the value must match |
step |
number, range |
Valid value granularity |
<!-- Minimum 8 characters, maximum 100 -->
<input type="password" name="password" required minlength="8" maxlength="100">
<!-- Number between 1 and 10 -->
<input type="number" name="rating" min="1" max="10" required>
<!-- US phone number pattern -->
<input type="tel" name="phone" pattern="\d{3}-\d{3}-\d{4}"
title="Format: 123-456-7890">
<!-- Date within range -->
<input type="date" name="event_date" min="2026-01-01" max="2026-12-31">
The title attribute provides a custom hint for pattern validation failures.
Per-Step Validation in Multi-Step Forms
In multi-step forms, clicking "Next" triggers validation only for fields in the current step panel. Fields in other steps are not validated until the user reaches them.
// Simplified per-step validation
function validateCurrentStep(form) {
const currentStep = form.querySelector('.cf-step-panel-active');
const fields = currentStep.querySelectorAll('[required], [pattern], [min], [max]');
let isValid = true;
fields.forEach(field => {
if (!validateField(field)) {
isValid = false;
}
});
if (!isValid) {
// Focus the first invalid field
const firstInvalid = currentStep.querySelector('.cf-invalid');
if (firstInvalid) firstInvalid.focus();
}
return isValid;
}
Fields hidden by conditional logic are skipped during validation. See conditional-logic.md for details.
Accessible Error Messaging
Core Forms follows WCAG 2.2 guidelines for form error presentation. Every validation error includes proper ARIA attributes so screen readers announce errors immediately.
Error Markup
When a field fails validation, Core Forms:
- Adds
aria-invalid="true"to the field. - Adds the
cf-invalidCSS class to the field. - Inserts an error message element with
role="alert". - Links the error to the field via
aria-describedby.
<!-- Before validation -->
<label for="email">Email</label>
<input type="email" name="email" id="email" required>
<!-- After failed validation -->
<label for="email">Email</label>
<input type="email" name="email" id="email" required
class="cf-invalid"
aria-invalid="true"
aria-describedby="email-error">
<div id="email-error" class="cf-field-error" role="alert">
Please enter a valid email address.
</div>
role="alert" Behavior
The role="alert" attribute causes screen readers to announce the error message as soon as it appears in the DOM, without requiring the user to navigate to it. This provides immediate feedback for assistive technology users.
aria-describedby
The aria-describedby attribute links the error message to its field. When a screen reader user focuses the field, the error is read along with the field label:
"Email, edit text, required. Please enter a valid email address."
Error Message Removal
When the user corrects an invalid field, Core Forms removes the error on the next blur or input event:
- Removes
aria-invalidattribute. - Removes the
cf-invalidclass. - Removes the error message element from the DOM.
- Removes the
aria-describedbyreference.
CSS Classes
.cf-invalid
Applied to any field that fails validation. Default styling adds a red border:
.cf-invalid {
border-color: #dc3545;
box-shadow: 0 0 0 1px #dc3545;
}
.cf-invalid:focus {
border-color: #dc3545;
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25);
}
.cf-field-error
The error message element displayed below invalid fields:
.cf-field-error {
color: #dc3545;
font-size: 0.875em;
margin-top: 0.25em;
display: block;
}
Customizing Error Styles
Override the default styles in your theme:
/* Custom error styling */
.cf-form .cf-invalid {
border-color: var(--color-error);
background-color: var(--color-error-bg);
}
.cf-form .cf-field-error {
color: var(--color-error);
font-size: 0.8rem;
font-weight: 500;
}
Default Error Messages
Core Forms provides sensible default messages for each validation type:
| Rule | Default Message |
|---|---|
| Required | "This field is required." |
| "Please enter a valid email address." | |
| Min (number) | "Value must be at least {min}." |
| Max (number) | "Value must be no more than {max}." |
| Minlength | "Please enter at least {minlength} characters." |
| Maxlength | "Please enter no more than {maxlength} characters." |
| Pattern | Custom title attribute, or "Please match the requested format." |
Custom Error Messages
Override messages per field using data-error-* attributes:
<input type="email" name="email" required
data-error-required="We need your email to send the report."
data-error-email="That doesn't look like a valid email.">
<input type="text" name="username" required minlength="3" maxlength="20"
pattern="^[a-zA-Z0-9_]+$"
data-error-required="Please choose a username."
data-error-minlength="Username must be at least 3 characters."
data-error-pattern="Only letters, numbers, and underscores allowed.">
Server-Side Validation
Client-side validation improves UX but is never trusted for security. Core Forms re-validates all fields on the server during Forms::process():
// Server-side validation runs these checks:
// 1. Honeypot field (must be empty)
// 2. Nonce verification
// 3. Required fields
// 4. Email format
// 5. Custom validation via cf_validate_form filter
// Add custom server-side validation
add_filter('cf_validate_form', function ($errors, $form, $data) {
if ($form->slug === 'registration') {
if (strlen($data['password']) < 8) {
$errors[] = [
'field' => 'password',
'message' => 'Password must be at least 8 characters.',
];
}
if ($data['password'] !== $data['password_confirm']) {
$errors[] = [
'field' => 'password_confirm',
'message' => 'Passwords do not match.',
];
}
}
return $errors;
}, 10, 3);
When server-side validation fails, the JSON response includes field-level errors that the frontend maps back to individual fields:
{
"success": false,
"data": {
"errors": [
{ "field": "password", "message": "Password must be at least 8 characters." },
{ "field": "password_confirm", "message": "Passwords do not match." }
]
}
}
The frontend then applies cf-invalid and error messages to the corresponding fields, exactly as with client-side validation.