Form Field Validation without JavaScript

Fionna Chan
5 min readJul 27, 2018

Ah! Not the FORM again!

Yes I am sorry, developers have to deal with forms from time to time, it’s unavoidable, that’s our life.

Remember the days when placeholders don’t work on IE? No? Sorry kiddo you are too young…. Since the day there’s more than one browser used to surf the Internet, it has always been painful to work on forms even for the simplest part like a placeholder, or targeting a focused form field, and of course the validation logic for the fields.

To rescue these annoyed web developers from stabbing IE browser developers (well at least I have done these countless times in my mind), entered HTML5 and CSS3!!! ❤❤❤

So let’s see how we can do form field validation WITHOUT JavaScript! It sounds like a dream even for 3 years ago, I know, but it is REAL GUYS AND GALS!

Our example will be a sign-in form with one field for email and one field for password.

Let’s do the standard thing: (I am using Pug here)

form
.form-row
input(type="email", id="login-email", name="login-email")
label(for="login-email") Email
.form-row
input(type="password", id="login-password", name="login-password")
label(for="login-email") Password
button(id="submit", type="submit") SUBMIT

I used .form-row and put the label after the input so that I can style the label like the way Material-UI does.

$transition-time: 0.3s;.form-row {
display: block;
position: relative;
width: 320px;
margin: 30px auto;

label {
user-select: none;
display: block;
position: absolute;
left: 18px;
transition: font-size $transition-time;
}
}
input[type="email"],
input[type="password"] {
-webkit-appearance: none;
width: 100%;
height: 60px;
padding: 16px 18px 0;
border: none;
resize: none;
outline: none;
}
input + label {
top: 8px;
font-size: 11px;
transform: none;
transition: top $transition-time, transform $transition-time;
}

If you are trying this out, you would notice that we haven’t fully recreated the label style in Material-UI, but I will explain that later.

In order to make sure the users fill in those two fields, we have to add this attribute to the input fields: “required”

form
.form-row
input(type="email", id="login-email", name="login-email", required)
label(for="login-email") Email
.form-row
input(type="password", id="login-password", name="login-password", required)
label(for="login-email") Password
button(id="submit", type="submit") SUBMIT

After adding the required attribute, when the user clicks the submit button without filling in these two fields with the proper format, they will see a validation message. For example, if the user typed something in the email field, but it is not a proper email with . and @, the error message will be:

Please include an “@” in the email address. ‘XXX’ is missing an ‘@’.

Please enter a part following ‘@’. ‘XXX@’ is incomplete.

(But apparently it is not perfect, because if the user typed ‘XXX@y’ it would pass the validation, so you have to add a regex pattern! I have to admit that I am not good at regex patterns, so I relied on the online community for the regex part.

form
.form-row
input(type="email", id="login-email", name="login-email", pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", required)
label(for="login-email") Email
.form-row
input(type="password", id="login-password", name="login-password", required)
label(for="login-email") Password
button(id="submit", type="submit") SUBMIT

So if the user wants to submit without a .something, this error message will show:

Please match the requested format.

Unfortunately, if you want to change the error message, you need some JavaScript help, and it will be out of context in this article, so I will skip it.

For the password field, we also add a pattern because we don’t want the user creates a password that is too short, which will be too easy to hack. At the same time we add a hint text so that the user will know that they need to create a password with at least 6 characters long.

form
.form-row
input(type="email", id="login-email", name="login-email", pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", required)
label(for="login-email") Email
.form-row
input(type="password", id="login-password", name="login-password", pattern=".{6,}", required)
label(for="login-email") Password
span.hint (min. 6 characters)
button(id="submit", type="submit") SUBMIT

The hint is only useful when the user is typing in the password field, so let’s add this CSS:

.hint {
display: none;
font-size: 11px;
input:focus ~ label & {
display: inline;
}
}

Now we would like to add some styles to the fields that don’t pass the validation, let’s say hi to this psuedo-class :invalid !

input:invalid + label {
top: 50%;
font-size: 20px;
transform: translate(0, -50%);
}

OK great, now we have the Material-UI kind of look where the label is at the left-center of the field, and when the field is valid the label will be smaller and be on the top-left corner of the field. But wait! What about 1) when the user is typing but the field is not valid yet, and 2) when the user is not on the field anymore (on blur / out of focus), there is some text, but still invalid? The label and the text will be layered at the same place and become unreadable.

For both cases, we have to detect whether the field is focused, and whether the field is empty. The 1) case is focused & not empty, and the 2) case is not focused & not empty.

Let’s check ALL cases:

  1. focused & not empty & valid
  2. not focused & not empty & valid
  3. focused & empty & not valid
  4. focused & not empty & not valid
  5. not focused & empty & not valid
  6. not focused & not empty & not valid

To check if the field is empty or not, we need to do a small hack:

form
.form-row
input(type="email", id="login-email", name="login-email", placeholder=" ", pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", required)
label(for="login-email") Email
.form-row
input(type="password", id="login-password", name="login-password", placeholder=" ", pattern=".{6,}", required)
label(for="login-email") Password
span.hint (min. 6 characters)
button(id="submit", type="submit") SUBMIT

This is for the use of the following psuedo-class in CSS: :placeholder-shown When the placeholder is shown, the field is empty, and this psuedo-class only works when the placeholder is set in the HTML, but since we have the label which works as the placeholder already, we do not want to put any value for the placeholder, so an empty space is used instead. Remember to check the browser compatibility if you want to use it in projects.

As for focused or not, we use :focus

input + label, // cases 1, 2, 4 & 6
input:focus:invalid:placeholder-shown + label { // case 3 (this is to override the next set of CSS because adding :focus increases the CSS specificity)
top: 8px;
font-size: 11px;
transform: none;
transition: top $transition-time, transform $transition-time;
}
input:invalid:placeholder-shown + label { // case 5
top: 50%;
font-size: 20px;
transform: translate(0, -50%);
}
input:invalid:not(:placeholder-shown) { // cases 4 & 6
border: 1px solid red;
}

YAY! Thanks for finishing this article!

CSS is just so amazing right?!

I added some additional styles to the finished Pen just for fun :P

Play around with my CodePen or create your own! Comment below to share your thoughts with me!

Follow me if you like this article! I would share my Front-end experiments whenever I have time ;)

--

--

Fionna Chan

Frontend Unicorn. I 💖 CSS & JavaScript. My brain occasionally runs out of memory so I need to pen down my thoughts. ✨💻🦄🌈.y.at