I was helping a student today with some form validation. She wanted a clean user interface, and great usability, and when an input type wasn't valid and/or when a field was left empty by the user, a red message should appear giving clear direction, but also a red border should decorate the specific field in order to draw attention to where the correction needed to be made.
Here is the resulting code with comments in case it's helpful to others as a clean, lightweight form validation solution using HTML, CSS, and Javascript with great usability and accessibility in mind. The HTML, CSS, and Javascript are all in one HTML file here but they can also be broken out using external .css and .js if preferred for your use case.
Below you can view a screenshot of how the completed webform should behave when activated:

Grab the Code on Github
Blogs aren't always the best or easiest ways to study code. If it's easier, I've added this code to a public Github Gist that I created. You can study the code below or go to Github for additional functionality with your own Github Desktop or other code management preferences.
The <head>
section contains the css code to style the validation messages.
The <body>
section contains the webform. Note you can turn novalidate
on or off depending on if you want to test your custom validation messages and functionality.
The <script>
section contains the Javascript to validate the webform on button click or tap.
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Webform with Validation</title> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
margin: 20px; | |
padding: 20px; | |
} | |
form { | |
max-width: 400px; | |
margin: auto; | |
} | |
.form-group { | |
margin-bottom: 15px; | |
} | |
.form-group label { | |
display: block; | |
margin-bottom: 5px; | |
} | |
.form-group input { | |
width: 100%; | |
padding: 8px; | |
box-sizing: border-box; | |
} | |
.error { /*used only if there is a validation issue */ | |
color: red; | |
font-size: 0.9em; | |
} | |
.invalid { /*used only if there is a validation issue */ | |
border: 2px solid red; | |
} | |
</style> | |
</head> | |
<!-- You can remove "novalidate" to let the browser auto-check the "required" fields. | |
Or you can use "novalidate" to be able to check the custom Javascript validation and messages. --> | |
<body> | |
<h1 style="text-align: center;">Webform with Validation</h1> | |
<form id="myForm" novalidate> | |
<div class="form-group"> | |
<label for="email">Email:</label> | |
<input type="email" id="email" name="email" required> | |
<span class="error"></span> | |
</div> | |
<div class="form-group"> | |
<label for="tel">Telephone:</label> | |
<input type="tel" id="tel" name="tel" pattern="\d{3}-\d{3}-\d{4}" required> | |
<span class="error"></span> | |
<small>Format: 123-456-7890</small> | |
</div> | |
<div class="form-group"> | |
<label for="date">Date:</label> | |
<input type="date" id="date" name="date" required> | |
<span class="error"></span> | |
</div> | |
<div class="form-group"> | |
<label for="time">Time:</label> | |
<input type="time" id="time" name="time" required> | |
<span class="error"></span> | |
</div> | |
<div class="form-group"> | |
<label for="number">Number:</label> | |
<input type="number" id="number" name="number" min="1" max="100" step="1" required> | |
<span class="error"></span> | |
</div> | |
<button type="submit">Submit</button> | |
</form> | |
<footer></footer> | |
<script> | |
document.getElementById('myForm').addEventListener('submit', function(event) { | |
const form = event.target; | |
let isValid = true; | |
// Check if required fields are filled, if not, add the red border. | |
const requiredFields = form.querySelectorAll('[required]'); | |
requiredFields.forEach(function(field) { | |
if (!field.value) { | |
isValid = false; | |
field.classList.add('invalid'); | |
field.nextElementSibling.textContent = 'This field is required.'; | |
} else { | |
field.classList.remove('invalid'); | |
field.nextElementSibling.textContent = ''; | |
} | |
}); | |
// JavaScript validation for email. Use regex to make sure it's a valid email format. | |
const email = document.getElementById('email'); | |
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
if (!emailPattern.test(email.value)) { | |
isValid = false; | |
email.classList.add('invalid'); | |
email.nextElementSibling.textContent = 'Invalid email format.'; | |
} else { | |
email.classList.remove('invalid'); | |
email.nextElementSibling.textContent = ''; | |
} | |
// JavaScript validation for telephone. Use regex to make sure it's a valid phone format. | |
const tel = document.getElementById('tel'); | |
const telPattern = /^\d{3}-\d{3}-\d{4}$/; | |
if (!telPattern.test(tel.value)) { | |
isValid = false; | |
tel.classList.add('invalid'); | |
tel.nextElementSibling.textContent = 'Invalid phone number format.'; | |
} else { | |
tel.classList.remove('invalid'); | |
tel.nextElementSibling.textContent = ''; | |
} | |
if (!isValid) { | |
event.preventDefault(); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Image(s): The wall of Stanley cups in a sports store in Chicago; screenshot of the working form; Greg O'Toole.