Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back.
In our last lesson, we built the core logic for form validation, identifying errors and logging them to the console. This was a crucial step in ensuring data integrity. Now, we'll close the loop by communicating those errors back to the user—a task that lies at the heart of user experience design.
Today's lesson focuses on the learning outcome: Provide visual feedback for form validation by dynamically adding and removing CSS classes to display error messages or input styles.
As a designer, you know that how a system communicates its state is critical. A form that silently fails is frustrating; one that clearly highlights issues and guides the user toward success feels helpful and well-designed. We will connect the validation logic we wrote to the user interface, making our form interactive and user-friendly.
We will cover:
- Defining CSS styles for valid and invalid input states.
- Using JavaScript to dynamically add and remove these CSS classes.
- Creating helper functions to show and hide error messages.
- Enhancing the user experience by clearing errors as the user corrects them.
- Briefly touching on accessibility to ensure our feedback works for all users.
1. The CSS Foundation for Visual Feedback
Before our JavaScript can show or hide anything, we need to define what "error" and "success" states look like in our CSS. The most common and effective pattern is to create dedicated CSS classes that we can toggle with JavaScript.
A typical setup involves:
- A class like
.erroror.is-invalidthat adds a red border to the input field. - A corresponding
.successor.is-validclass for a green border. - Styling for the error message element itself (e.g., red text color).
The video below demonstrates how to create simple CSS rules for these states.
Form validation using Javascript on the client side for beginners
This video from JavaScript Academy shows a clear, standard way to set up CSS for validation feedback. We'll focus on the CSS portion.
Watch from 00:04:40 to 00:05:28. Pay attention to how the .input-control.success input and .input-control.error input rules are defined to change the border color. This is the visual foundation we will control with JavaScript.
2. Toggling Styles with JavaScript
With our CSS classes ready, the next step is to use JavaScript to apply them based on our validation logic. The primary tool for this is the element.classList property, which has methods like add(), remove(), and toggle().
To keep our code organized and reusable, we'll create helper functions—one to show an error and one to clear it (or show success).
The same video provides a great example of this pattern. It creates setError() and setSuccess() functions that handle two things at once:
- Adding/removing the appropriate class on the input's container.
- Setting the text content of the error message element.
Form validation using Javascript on the client side for beginners
Let's continue with the same video to see how to write the JavaScript functions that will apply our new CSS classes.
Watch from 00:06:37 to 00:07:43. Observe how the setError() function finds the parent element to add the 'error' class and sets the innerText of the error display. Notice how setSuccess() does the opposite, clearing the message and swapping the classes.
This approach of encapsulating the DOM manipulation in small, focused functions is a fundamental principle of clean code.
3. Integrating Visual Feedback into Our Validator
Now, we can upgrade the validation script we worked on in the previous lesson. Instead of just logging errors to the console, we'll call our new showError() and clearError() functions.
Let's take the solution from last lesson's practice exercise and modify it.
Original Logic (from previous lesson):
// Inside the validation loop...
if (!option.isValid(input)) {
console.error(option.errorMessage(input)); // We will replace this line
isValid = false;
break;
}
New Logic with Visual Feedback:
We'll create showError(input, message) and clearError(input) functions. Then, we'll update our validation loop to use them.
// Helper functions to manage the UI
function showError(input, message) {
const formGroup = input.parentElement;
const errorElement = formGroup.querySelector('.error-message');
formGroup.classList.add('error');
formGroup.classList.remove('success');
errorElement.textContent = message;
}
function clearError(input) {
const formGroup = input.parentElement;
const errorElement = formGroup.querySelector('.error-message');
formGroup.classList.remove('error');
formGroup.classList.add('success');
errorElement.textContent = '';
}
// Updated validation loop
const validateSingleInput = (input) => {
let errorFound = false;
// ... loop through validationOptions ...
if (!option.isValid(input)) {
showError(input, option.errorMessage(input)); // NEW
errorFound = true;
break;
}
if (!errorFound) {
clearError(input); // NEW
}
return !errorFound;
};
Note: In the video, the classes were applied to a parent div. In our practice code, the structure is slightly different, but the principle is the same. The code above is adapted for our exercise's HTML.
4. Enhancing the User Experience
Showing an error on submission is good, but a great user experience is more responsive.
Clearing Errors on Input
Once an error is displayed, it should disappear as soon as the user starts correcting their mistake. We can achieve this by listening for the input event on each field.
Login & Signup with HTML, CSS, JavaScript (form validation)
This video from Coding2GO demonstrates a simple but powerful UX improvement: removing the error state as the user types.
Watch from 00:27:22 to 00:28:15. The key idea is to add an 'input' event listener to each form field. Inside the listener, it checks if the element has the 'incorrect' class and removes it. This provides immediate, positive feedback to the user.
A Note on CSS Specificity
Have you ever applied a style that just didn't show up? A common reason is CSS specificity. An error style (e.g., a red border) might be overridden by a more specific style, like one for :hover or :focus. It's a classic "gotcha" for developers.
The same video has an excellent explanation of this problem and how to solve it by making your error style's selector more specific.
Login & Signup with HTML, CSS, JavaScript (form validation)
Let's look at a practical problem-solving tip related to styling error states.
Watch from 00:19:08 to 00:20:16. The narrator discovers that the hover state overrides the error state. Understand the explanation of why this happens (specificity) and how they fix it by making the CSS selector for the error state more specific (form .incorrect input instead of just .incorrect input).
5. Your Turn: Implement Visual Feedback
It's time to bring your form to life. Below is the same sign-up form from the last lesson, but this time your goal is to display the validation errors in the UI.
Your Goal:
Modify the JavaScript to implement and use showError() and clearError() functions. When the form is submitted with invalid data, the corresponding input fields should get a red border and display a relevant error message below them.
The Starter Code:
The JavaScript block contains the complete solution from the previous lesson. You will be modifying it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Visual Feedback Practice</title>
<style>
body { font-family: sans-serif; display: flex; justify-content: center; padding-top: 40px; background-color: #f4f4f9; }
.form-container { background: #fff; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 400px; }
h2 { margin-top: 0; text-align: center; }
.form-group { margin-bottom: 1.5rem; position: relative; }
label { display: block; margin-bottom: 0.5rem; font-weight: bold; }
input { width: 100%; padding: 0.75rem; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
button { width: 100%; padding: 0.75rem; border: none; border-radius: 4px; background-color: #007bff; color: white; font-size: 1rem; cursor: pointer; }
/* --- Styles for Validation Feedback --- */
.error-message {
color: #e74c3c;
font-size: 0.875rem;
margin-top: 0.25rem;
display: none; /* Hidden by default */
}
.form-group.error .error-message {
display: block; /* Shown when parent has .error class */
}
.form-group.error input {
border-color: #e74c3c;
}
.form-group.success input {
border-color: #2ecc71;
}
</style>
</head>
<body>
<div class="form-container">
<h2>Create Account</h2>
<form id="signup-form" novalidate>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required minlength="3">
<div class="error-message"></div>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
<div class="error-message"></div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required minlength="8">
<div class="error-message"></div>
</div>
<div class="form-group">
<label for="password-confirm">Confirm Password</label>
<input type="password" id="password-confirm" name="password-confirm" required data-match="password">
<div class="error-message"></div>
</div>
<button type="submit">Sign Up</button>
</form>
</div>
<script>
const form = document.querySelector('#signup-form');
// --- PREVIOUS LESSON'S CODE (Your starting point) ---
const getLabelText = (input) => input.previousElementSibling.textContent;
const validationOptions = [
{
attribute: 'required',
isValid: input => input.value.trim() !== '',
errorMessage: (input) => `${getLabelText(input)} is required.`,
},
{
attribute: 'minlength',
isValid: input => input.value.length >= parseInt(input.minLength, 10),
errorMessage: (input) => `${getLabelText(input)} must be at least ${input.minLength} characters.`,
},
{
attribute: 'type',
isValid: input => input.type !== 'email' || !input.validity.typeMismatch,
errorMessage: (input) => `Please enter a valid ${getLabelText(input).toLowerCase()}.`,
},
{
attribute: 'data-match',
isValid: input => {
const matchInput = form.querySelector(`#${input.dataset.match}`);
return input.value === matchInput.value;
},
errorMessage: (input) => `${getLabelText(input)} must match the password.`,
}
];
const validateSingleInput = (input) => {
let isValid = true;
for (const option of validationOptions) {
if (input.hasAttribute(option.attribute) || (option.attribute === 'type' && input.type === 'email')) {
if (!option.isValid(input)) {
console.error(option.errorMessage(input)); // <-- TODO: Replace this!
isValid = false;
break;
}
}
}
return isValid;
};
form.addEventListener('submit', (event) => {
event.preventDefault();
const inputs = form.querySelectorAll('input');
let isFormValid = true;
inputs.forEach(input => {
if (!validateSingleInput(input)) {
isFormValid = false;
}
});
if (isFormValid) console.log('Form is valid!');
});
// --- YOUR TASK ---
// 1. Create a function `showError(input, message)`. It should:
// - Find the input's parent `.form-group`.
// - Add the 'error' class and remove the 'success' class from the `.form-group`.
// - Find the `.error-message` element within that group and set its text content to the message.
// 2. Create a function `clearError(input)`. It should:
// - Find the input's parent `.form-group`.
// - Add the 'success' class and remove the 'error' class from the `.form-group`.
// - Find the `.error-message` element and set its text content to an empty string.
// 3. Modify `validateSingleInput` to use your new functions.
// - If an input is invalid, call `showError`.
// - If an input is valid after all checks, call `clearError`.
// - Remember to return `true` or `false` for the input's validity.
// YOUR CODE HERE
</script>
</body>
</html>
Click here for a complete solution to guide you
// --- YOUR CODE HERE ---
// 1. Create a function `showError(input, message)`
function showError(input, message) {
const formGroup = input.parentElement;
const errorElement = formGroup.querySelector('.error-message');
formGroup.classList.add('error');
formGroup.classList.remove('success');
errorElement.textContent = message;
}
// 2. Create a function `clearError(input)`
function clearError(input) {
const formGroup = input.parentElement;
const errorElement = formGroup.querySelector('.error-message');
formGroup.classList.remove('error');
formGroup.classList.add('success');
errorElement.textContent = '';
}
// 3. Modify `validateSingleInput`
const newValidateSingleInput = (input) => {
let errorFound = false;
for (const option of validationOptions) {
if (input.hasAttribute(option.attribute) || (option.attribute === 'type' && input.type === 'email')) {
if (!option.isValid(input)) {
showError(input, option.errorMessage(input));
errorFound = true;
break; // Stop at the first error for this input
}
}
}
if (!errorFound) {
clearError(input);
}
return !errorFound;
};
// Replace the old event listener logic with the new one
form.addEventListener('submit', (event) => {
event.preventDefault();
const inputs = form.querySelectorAll('input');
let isFormValid = true;
inputs.forEach(input => {
if (!newValidateSingleInput(input)) {
isFormValid = false;
}
});
if (isFormValid) {
console.log('Form is valid! Submitting...');
alert('Success! Form submitted.');
} else {
console.log('Form has errors.');
}
});
// Bonus: Clear errors on input
const allInputs = form.querySelectorAll('input');
allInputs.forEach(input => {
input.addEventListener('input', () => {
// As the user types, we can validate in real-time
newValidateSingleInput(input);
});
});
6. A Quick Word on Accessibility
Visual feedback like red borders and text is great, but it's inaccessible to users who rely on screen readers. To create a truly inclusive experience, we should also programmatically announce errors.
This is done using ARIA (Accessible Rich Internet Applications) attributes. The two most important for form validation are:
aria-invalid="true": Tells a screen reader that the input's value is invalid.aria-describedby="error-id": Links the input to the element containing its error message, so the screen reader can announce it.
Let's see how we can incorporate this into our helper functions.
Vanilla JavaScript Form Handling: No Framework Guide
This article from strapi.io shows a robust way to handle errors, including accessibility. We'll focus on the showError and clearError functions.
Read the code block under the 'Display Error Messages' heading. Notice how the showError function not only adds a class but also sets aria-describedby. The clearError function removes it. This is a best practice for making your forms accessible.
Integrating these ARIA attributes into your showError and clearError functions ensures that your validation feedback is communicated to all users, not just sighted ones.
Conclusion
Fantastic work today! You've bridged the gap between abstract logic and tangible user interface, a skill that is fundamental to front-end development and directly leverages your expertise in user experience.
Key Takeaways:
- Visual feedback for validation is managed by defining CSS classes (
.error,.success) and using JavaScript'selement.classListto toggle them. - Creating helper functions like
showErrorandclearErrormakes your code cleaner, more readable, and easier to maintain. - A superior user experience involves not only showing errors on submission but also providing real-time feedback, such as clearing an error state when the user starts typing.
- Always consider accessibility by using ARIA attributes like
aria-invalidandaria-describedbyto support screen reader users.
In our next lesson, we'll explore some modern JavaScript syntax features, starting with template literals, which will help us construct strings and messages more elegantly.