Build WCAG-compliant forms with proper labels, validation, and error handling
<form
id="contact-form"
class="accessible-form"
method="post"
action="#"
novalidate
aria-labelledby="contact-form-title"
>
<h2 id="contact-form-title">Contact Form</h2>
<div class="form-group">
<label for="name" class="form-label">
Name
<span class="required" aria-hidden="true">*</span><span class="sr-only">(required)</span>
</label>
<input
type="text"
id="name"
name="name"
class="form-input"
required aria-required="true"
aria-invalid="false"
/>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
Submit
</button>
</div>
<!-- Live region for form errors -->
<div role="alert" aria-live="assertive" aria-atomic="true" class="form-alert" id="contact-form-alert"></div>
</form>.accessible-form {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #1f2937;
}
.form-label .required {
color: #dc2626;
margin-left: 0.25rem;
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 0.75rem;
border: 2px solid #d1d5db;
border-radius: 0.375rem;
font-size: 1rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input:invalid:not(:focus),
.form-textarea:invalid:not(:focus),
.form-select:invalid:not(:focus) {
border-color: #dc2626;
}
.form-input[aria-invalid="true"],
.form-textarea[aria-invalid="true"],
.form-select[aria-invalid="true"] {
border-color: #dc2626;
}
.form-help-text {
display: block;
margin-top: 0.5rem;
font-size: 0.875rem;
color: #6b7280;
}
.form-error {
display: none;
margin-top: 0.5rem;
font-size: 0.875rem;
color: #dc2626;
}
.form-error.show {
display: block;
}
.checkbox-group,
.radio-group {
margin-bottom: 0.5rem;
}
.checkbox-label,
.radio-label {
display: flex;
align-items: center;
font-weight: normal;
cursor: pointer;
}
.checkbox-input,
.radio-input {
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
cursor: pointer;
}
.checkbox-input:focus,
.radio-input:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
.btn {
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 0.375rem;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
}
.btn:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.btn:active {
transform: scale(0.98);
}
.btn-primary {
background-color: #3b82f6;
color: white;
}
.btn-primary:hover {
background-color: #2563eb;
}
.btn-secondary {
background-color: #6b7280;
color: white;
}
.btn-secondary:hover {
background-color: #4b5563;
}
.form-alert {
margin-top: 1rem;
padding: 1rem;
border-radius: 0.375rem;
background-color: #fee2e2;
border: 2px solid #dc2626;
color: #991b1b;
display: none;
}
.form-alert:not(:empty) {
display: block;
}
/* Screen reader only text */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}// Form validation and accessibility enhancements
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('contact-form');
const alertBox = document.getElementById('contact-form-alert');
form.addEventListener('submit', function(event) {
event.preventDefault();
// Clear previous errors
const errors = form.querySelectorAll('.form-error');
errors.forEach(error => error.classList.remove('show'));
const inputs = form.querySelectorAll('[aria-invalid]');
inputs.forEach(input => input.setAttribute('aria-invalid', 'false'));
alertBox.textContent = '';
// Validate form
let isValid = true;
let errorMessages = [];
// Validate name
const nameField = document.getElementById('name');
if (nameField) {
if (nameField.hasAttribute('required') && !nameField.value.trim()) {
nameField.setAttribute('aria-invalid', 'true');
const errorEl = document.getElementById('name-error');
if (errorEl) errorEl.classList.add('show');
errorMessages.push('Name');
isValid = false;
}
}
if (!isValid) {
// Show errors in alert box
alertBox.textContent = 'Please correct the errors in the form: ' + errorMessages.join(', ');
// Focus first error
const firstError = form.querySelector('[aria-invalid="true"]');
if (firstError) {
firstError.focus();
}
return false;
}
// Form is valid - submit
alertBox.textContent = '';
console.log('Form submitted successfully!');
// In real implementation, submit to server
// form.submit();
});
// Real-time validation on blur
form.querySelectorAll('.form-input, .form-textarea, .form-select').forEach(input => {
input.addEventListener('blur', function() {
validateField(this);
});
});
function validateField(field) {
const errorElement = document.getElementById(field.id + '-error');
if (field.hasAttribute('required') && !field.value.trim()) {
field.setAttribute('aria-invalid', 'true');
if (errorElement) {
errorElement.classList.add('show');
}
} else if (field.validity && !field.validity.valid) {
field.setAttribute('aria-invalid', 'true');
if (errorElement) {
errorElement.classList.add('show');
}
} else {
field.setAttribute('aria-invalid', 'false');
if (errorElement) {
errorElement.classList.remove('show');
}
}
}
});<label for="email">Email Address</label>
<input type="email" id="email" name="email" /><label for="name">
Name
<span aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</label>
<input type="text" id="name" required aria-required="true" /><input
type="email"
id="email"
aria-invalid="true"
aria-describedby="email-error"
/>
<span id="email-error" role="alert">
Please enter a valid email address
</span><label for="password">Password</label>
<input
type="password"
id="password"
aria-describedby="password-help"
/>
<span id="password-help">
Must be at least 8 characters
</span>Help others discover this tool!