Exploring Unconventional Styling

CSS Form Validation

Daniel Fuller
Daniel Fuller Founder & Tech Director, Danimate LLC Family man + CSS enthusiast

Motivation

Forms are everywhere on the web; there’s no avoiding them. And default forms are ugly. However, There’s a lot you can do with CSS to make forms prettier, more useful, more helpful, and easier to fill out. In this post we’ll push the limits a bit of what CSS can do with form inputs. And as usual, no Javascript will be used.

Accessibility

First, here’s a note on accessibility. It’s hard to find stats on how many people use screen readers or other tools to help them use the internet. But, even if it’s only a few million, making your site more accessible to them will help you stand out from other sites. A little effort actually goes a long way.

HTML provides several tags and attributes that screen readers rely on: <label> tags, aria attributes and role attributes. Here’s a breakdown:

Element Description
Label Tag Adds context to a form input e.g. ‘First Name’
area-label attribute Like a label tag, but can be used on any element
aria-required Indicates a field must be filled out
aria-describedBy ‘Links’ other elements that describe or add context, such as an error message or hint text
aria-invalid Marks a field as invalid so Assistive Tech knows why a form didn’t submit
role Indicate the role of the element

We’ll use all these in our markup, except for aria-invalid which would need to be added with JS or from a server-side render after validation. You’re doing server-side validation always, right?

Basic Form Styles

Let’s add some basic form elements to a new HTML file. We’ll use nested inputs in labels to reduce our markup (nested markup or for/id attributes are both WCAG compliant).

<body>
  <form>
    <label for="first_name">First Name
      <input type="text" id="first_name" name="first_name" placeholder="Enter Name Here" required/>
    </label>
    <label for="email_address">Email Address
      <input type="email" id="email_address" name="email_address" placeholder="Email" required/>
    </label>
    <input type="submit" value="Create Account" />
  </form>
</body>

Add some styles:

label {
  display: block;
  position: relative;
  font-size: .9em;
  font-weight: 600;

  & + label {
    margin-top: 30px;
  }

  input {
    display: block;
    box-sizing: border-box;
    min-width: 220px;
    border: 1px solid lightgrey;
    border-left: 5px solid lightgrey;
    border-radius: 5px;
    outline: none;
    padding: 10px 15px;
  }

  input[type=text]:placeholder-shown,
  input[type=email]:placeholder-shown {
    border-color: lightgrey;
  }
}

and now we have a simple form.

Basic form with name and email

Focus State

Use :focus to select an element that currently has focus, meaning the user has clicked, tapped, tabbed to, or in some other way indated that they want to interact with that element. Let’s make our input border change when focused

label {
  input[type=text]:focus,
  input[type=email]:focus {
    border-color: lightskyblue;
    box-shadow: 0 2px 5px 0 rgba(0,0,0,0.1);
  }
}

Basic form with name and email and name focused

Now the user can easily tell which element they are interacting with. This is really important for users who are tabbing through the form to fill out the fields.

HTML Validation

I like HTML validation, though it gets plenty of hate (especially from designers :). It isn’t very customizable visually, but it works reliably. It has a bunch of built-in validation types and is triggered natively when the form submits. It also stays out of the way while the user is filling out the form, and only appears if the user had a problem filling out the form. If so, an error is shown and is updated in real-time as the user corrects the issue.

Next we explore a few fields with built-in validations and how to customize them.

Requiredness

You may have noticed in the HTML above that we used the required attribute on the inputs. This will cause the browser to validate these fields when the form is submitted. Let’s add a submit button and indicate to the user that these fields are required.

<input type="submit" value="Create Account" />
input {
  display: block;
  box-sizing: border-box;
  min-width: 220px;
  border-radius: 5px;
  outline: none;
  padding: 10px 15px;
}

input:not([type=submit]) {
  border: 1px solid lightgrey;
  border-left: 5px solid lightgrey;
}

input[type=submit] {
  background: #4A90E2;
  color: white;
  margin-top: 30px;
  border: none;
  cursor: pointer;

  &:hover {
    background: #357ABD;
  }
}

input:not([type=submit])[type=text]:placeholder-shown,
input:not([type=submit])[type=email]:placeholder-shown {
  border-color: lightgrey;
}

input:not([type=submit])[type=text]:focus,
input:not([type=submit])[type=email]:focus {
  border-color: lightskyblue;
  box-shadow: 0 2px 2px 0 rgba(0,0,0,0.2);
}

input[required] {
  background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="lightgrey" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="17" y1="7" x2="7" y2="17"></line><line x1="7" y1="7" x2="17" y2="17"></line><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>');
  background-repeat: no-repeat;
  background-position: right 5px center;
  background-size: 20px;
}

Basic form with name and email and required indicator

If you try to submit the form right now, the browser notices there are required fields without a value and helpfully generates a message to inform the user. In Chrome on MacOS it looks like the following image, but every browser and OS can display the errors differently. The important thing is that they all do.

Basic form with name and email and required indicator with error state

We should reward the user for filling out the fields properly! The border should turn green when they have entered a valid value.

input:not([type=submit]):valid:not(:placeholder-shown) {
  border-color: rgb(118, 189, 118);
  border-left: 5px solid rgb(118, 189, 118);
}

Basic form with name and email and required indicator with validity visible

Let’s move on to some other validation types.

Email

HTML5 defined a new type for input called email which comes with a validator. The HTML we used above already has type='email' so let’s see if it works.

Basic form with name and email and required indicator with error state on email address

Nice! But it looks too happy to be an error.

input:not([type=submit]):invalid:not(:placeholder-shown) {
  border-color: #EFADB5;
}

And now it looks like a proper error that needs attention.

Basic form with name and email and required indicator with error state on email address

Password

We’ve seen required validation and email field validation performed by the browser. You can also do custom validations using RegEx patterns. Let’s add a password field that is required, but also has additional content requirements.

<label for="password">Password
  <input type="text" id="password" name="password" placeholder="Enter a strong pasword" required pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}"/>
</label>

HTML5 validations again know what to do and show an error message when the form is submitted.

Basic form with unhelpful error state on password

That isn’t very helpful! We’ll add hints in a minute to help the user know what is expected for each field. First let’s cover a few more field types and their styles.

Number

Number fields can have a min, a max and a step value defined.

<label for="password">Password
  <input type="text" id="password" name="password" placeholder="Enter a strong pasword" required pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}"/>
</label>

As you can see, validations just work automatically.

Select

Styling a select box can be tricky with CSS, as in most browsers there is always something selected in a select box - usually the first option by default. So I guess the best we can do with this CSS route is to mark them as valid.

form select {
  border-color: rgb(118, 189, 118);
  border-left: 5px solid rgb(118, 189, 118);
}

Basic form with validations

Let’s move on to hints to help the user as they’re filling out this form.

Field Hints

Let’s add hint text next to each input field in our HTML markup.

<form>
  <label for="first_name">First Name
    <input type="text" id="first_name" name="first_name" placeholder="Enter Name Here" required/>
    <span class='hint'>We'll use this to address you</span>
  </label>
  <label for="email_address">Email Address
    <input type="email" id="email_address" name="email_address" placeholder="Email" required/>
    <span class='hint'>You will use this email address to log in</span>
  </label>
  <label for="password">Password
    <input type="password" id="password" name="password" placeholder="Enter a strong pasword" required pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}"/>
    <span class='hint'>Passwords need 8 chars (with upper, lower and number)</span>
  </label>        
  <label for="age">Age (optional)
    <input type="number" id="age" name="age" placeholder="What is your age?" min="13" max="120"/>
    <span class='hint'>You must be at least 13 years old to use this service</span>
  </label>
  <label for="gender">Gender
    <select name="gender" title="Select your gender" required>
      <option value="male">Male</option>
      <option value="female">Female</option>
    </select>
    <span class='hint'>For statistical purposes</span>
  </label>

  <input type="submit" value="Create Account" />
</form>

Now we’ll style them away until their associated field has focus. That way the UI isn’t cluttered up with a bunch of hint text.

.hint {
  display: block;
  opacity: 0;
  font-size: 0.9em;
  font-style: italic;
  color: #666;
  position: absolute;
  left: 110%;
  width: max-content;
  bottom: 10px;
  transition: opacity 0.2s ease-in-out, left 0.2s ease-in-out;
}

input:focus + .hint,
select:focus + .hint {
  opacity: 1;
  left: 105%;
}

With this, we give the user enough information to hopefully fill out the form perfectly the first time. If not, HTML validation will guide them through getting it right.

Also, always validate user input server-side! Front-end validation is never enough.

Here’s the final product

Basic form with validations animated

Conclusion

With CSS and a simple HTML structure, many forms will just work right out-of-the-box. For more complicated forms (like with many conditional validations) a JS library would probably be preferred.

You can view the CSS Form Validation and Styling Example Page or you can check out the CSS Form Validation and Styling repo on Github.