Contact Form Validation and Success Feedback
Module 06 introduced form validation as a concept. This lesson builds the full, production-ready version with better UX: errors clear when the user starts correcting a field, email format is validated beyond a simple non-empty check, and the success state replaces the entire form rather than just appending a message.
What it does when complete
Section titled “What it does when complete”- Submitting the empty form shows inline errors under each required field
- Errors clear automatically when the user starts typing in the problem field
- Submitting with an email that lacks
@or.shows a specific format error - Message must be 500 characters or fewer — submitting over the limit shows an error
- Submitting with all valid fields hides the form and shows a success message
- The success message has a “Send another” button that restores the form
Step 1 — Select form elements
Section titled “Step 1 — Select form elements”main.js runs on every page, so querying .contact-form at module scope returns null on pages that don’t have the form. All form-related element selections and listeners must go inside the if (contactForm) guard. Select elements first, then attach listeners inside the block:
const contactForm = document.querySelector('.contact-form');const successMessage = document.querySelector('.form-success');
if (contactForm) { const nameInput = contactForm.querySelector('#contact-name'); const emailInput = contactForm.querySelector('#contact-email'); const messageInput = contactForm.querySelector('#contact-message'); const nameError = contactForm.querySelector('#name-error'); const emailError = contactForm.querySelector('#email-error'); const messageError = contactForm.querySelector('#message-error');
// listeners go here}Expected HTML structure for each field (from contact.html):
<div class="form-group"> <label for="contact-name">Your name</label> <input type="text" id="contact-name" name="name" required> <span class="field-error" id="name-error" aria-live="polite"></span></div>aria-live="polite" announces error messages to screen readers when they appear.
Step 2 — Submit handler
Section titled “Step 2 — Submit handler”Inside the if (contactForm) block, read values fresh from the inputs on each submission:
contactForm.addEventListener('submit', (event) => { event.preventDefault(); clearErrors();
const name = nameInput.value.trim(); const email = emailInput.value.trim(); const message = messageInput.value.trim();
let isValid = true;
if (!name) { showError(nameError, 'Name is required.'); isValid = false; }
if (!email) { showError(emailError, 'Email is required.'); isValid = false; } else if (!email.includes('@') || !email.includes('.')) { showError(emailError, 'Enter a valid email address.'); isValid = false; }
if (!message) { showError(messageError, 'Message is required.'); isValid = false; } else if (message.length > 500) { showError(messageError, 'Message must be 500 characters or fewer.'); isValid = false; }
if (!isValid) return;
contactForm.reset(); updateCharCount(0); showSuccess();});contactForm.reset() clears all field values. updateCharCount(0) resets the character counter to zero — it is defined at module scope earlier in main.js alongside the char-count section. It must be module-scope (not inside an if block) to be callable here.
Step 3 — Helper functions
Section titled “Step 3 — Helper functions”These three functions live at module scope so they are available across the contact form section:
function showError(errorEl, message) { if (errorEl) { errorEl.textContent = message; errorEl.classList.add('visible'); }}
function clearErrors() { document.querySelectorAll('.field-error').forEach(el => { el.textContent = ''; el.classList.remove('visible'); });}
function showSuccess() { if (contactForm) { contactForm.style.display = 'none'; if (successMessage) { successMessage.classList.add('visible'); } }}clearErrors uses querySelectorAll('.field-error') rather than a hardcoded list — it clears every error span on the page without needing references to each one.
Step 4 — Clear errors as the user types
Section titled “Step 4 — Clear errors as the user types”Inside the if (contactForm) block, each input handler checks whether the field is now valid before clearing the error. Errors only update if they are already visible — no premature errors appear while the user is filling out a fresh form.
.field-error has display: block in CSS at all times — errors are hidden by having empty textContent, not by the absence of the visible class. Clearing an error requires setting textContent = '' in addition to removing the class, matching what clearErrors() does.
nameInput.addEventListener('input', () => { if (nameInput.value.trim()) { nameError.textContent = ''; nameError.classList.remove('visible'); }});
emailInput.addEventListener('input', () => { const email = emailInput.value.trim(); if (email.includes('@') && email.includes('.')) { emailError.textContent = ''; emailError.classList.remove('visible'); } else if (emailError.classList.contains('visible')) { showError(emailError, email ? 'Enter a valid email address.' : 'Email is required.'); }});
messageInput.addEventListener('input', () => { const message = messageInput.value.trim(); if (message && message.length <= charLimit) { messageError.textContent = ''; messageError.classList.remove('visible'); } else if (messageError.classList.contains('visible')) { showError(messageError, message ? 'Message must be 500 characters or fewer.' : 'Message is required.'); }});The emailError.classList.contains('visible') guard on the else branches means the error message can update in real time (e.g., switching from “Email is required.” to “Enter a valid email address.” as the user types) without ever showing an error on a field the user has not yet submitted.
Step 5 — Success state with reset
Section titled “Step 5 — Success state with reset”The success message HTML is already in contact.html:
<div class="form-success"> <p>Thank you! Your message has been sent.</p> <button class="btn-reset-form">Send another message</button></div>Wire the reset button to restore the form. This goes outside the if (contactForm) block because .btn-reset-form lives in .form-success, not inside the form element:
const resetBtn = document.querySelector('.btn-reset-form');if (resetBtn) { resetBtn.addEventListener('click', () => { contactForm.reset(); contactForm.style.display = ''; if (successMessage) successMessage.classList.remove('visible'); });}contactForm.style.display = '' removes the inline style set by showSuccess(), restoring whatever CSS controls.
Complete feature code
Section titled “Complete feature code”updateCharCount must be declared at module scope (outside any if block) so the submit handler can call updateCharCount(0) after a successful submission.
// updateCharCount is declared at module scope in the char-count section above// const charLimit = 500; (also module scope)
const contactForm = document.querySelector('.contact-form');const successMessage = document.querySelector('.form-success');
function showError(errorEl, message) { if (errorEl) { errorEl.textContent = message; errorEl.classList.add('visible'); }}
function clearErrors() { document.querySelectorAll('.field-error').forEach(el => { el.textContent = ''; el.classList.remove('visible'); });}
function showSuccess() { if (contactForm) { contactForm.style.display = 'none'; if (successMessage) successMessage.classList.add('visible'); }}
if (contactForm) { const nameInput = contactForm.querySelector('#contact-name'); const emailInput = contactForm.querySelector('#contact-email'); const messageInput = contactForm.querySelector('#contact-message'); const nameError = contactForm.querySelector('#name-error'); const emailError = contactForm.querySelector('#email-error'); const messageError = contactForm.querySelector('#message-error');
contactForm.addEventListener('submit', (event) => { event.preventDefault(); clearErrors();
const name = nameInput.value.trim(); const email = emailInput.value.trim(); const message = messageInput.value.trim(); let isValid = true;
if (!name) { showError(nameError, 'Name is required.'); isValid = false; }
if (!email) { showError(emailError, 'Email is required.'); isValid = false; } else if (!email.includes('@') || !email.includes('.')) { showError(emailError, 'Enter a valid email address.'); isValid = false; }
if (!message) { showError(messageError, 'Message is required.'); isValid = false; } else if (message.length > 500) { showError(messageError, 'Message must be 500 characters or fewer.'); isValid = false; }
if (!isValid) return;
contactForm.reset(); updateCharCount(0); showSuccess(); });
nameInput.addEventListener('input', () => { if (nameInput.value.trim()) { nameError.textContent = ''; nameError.classList.remove('visible'); } });
emailInput.addEventListener('input', () => { const email = emailInput.value.trim(); if (email.includes('@') && email.includes('.')) { emailError.textContent = ''; emailError.classList.remove('visible'); } else if (emailError.classList.contains('visible')) { showError(emailError, email ? 'Enter a valid email address.' : 'Email is required.'); } });
messageInput.addEventListener('input', () => { const message = messageInput.value.trim(); if (message && message.length <= charLimit) { messageError.textContent = ''; messageError.classList.remove('visible'); } else if (messageError.classList.contains('visible')) { showError(messageError, message ? 'Message must be 500 characters or fewer.' : 'Message is required.'); } });}
const resetBtn = document.querySelector('.btn-reset-form');if (resetBtn) { resetBtn.addEventListener('click', () => { contactForm.reset(); contactForm.style.display = ''; if (successMessage) successMessage.classList.remove('visible'); });}Step 6 — Verify
Section titled “Step 6 — Verify”- Submit empty — all three errors appear
- Start typing in the name field — name error disappears
- Enter an email without
@— format error shows - Paste a message over 500 characters and submit — length error shows
- Fix all fields and submit — form hides, success message shows, character count resets to 0
- Click “Send another message” — form reappears, fields are empty
- Element selections for the form live inside
if (contactForm)—main.jsloads on every page, so a module-scope query returnsnullon pages without the form. - The submit handler reads
.value.trim()fresh on each submission — values captured at page load would always be empty strings. clearErrors()usesquerySelectorAll('.field-error')— no hardcoded list of error elements to maintain.updateCharCountmust be declared at module scope (not inside anifblock) so the submit handler can call it after a successful submission.- Input handlers check validity before clearing — clearing requires
textContent = ''in addition to removing the class, because.field-errorhasdisplay: blockalways and errors are hidden by empty content, not by the class. TheemailError.classList.contains('visible')guard means error messages update in real time after a submit, but no premature error appears on a fresh field. contactForm.reset()clears fields;updateCharCount(0)syncs the character counter; thenshowSuccess()swaps the UI.- The reset button restores
display: ''(removes the inline style) rather than settingdisplay: 'block'— lets CSS control layout normally.