93

I recently discovered that the :invalid pseudo-class applies to required form elements as soon as the page loads. For example, if you have this code:

<style>
input:invalid { background-color: pink; color: white; }
input:valid { background-color: white; color: black; }
</style>
…
<input name="foo" required />

Then your page will load with an empty pink input element on it. Having validation built in to HTML5 is great, but I don't think most users expect the form to validate before they've had a chance to enter any values at all. Is there any way to delay the application of the pseudo-class until the first event affecting that element (form submit, blur, change, whatever's appropriate)? Is it possible to do this without JavaScript?

kojiro
  • 74,557
  • 19
  • 143
  • 201
  • I can't comment, so I'll add this supplement to @user3284463 The `makeDirty` should not toggle the dirty class, it should keep it on future (blue, invalid, valid) events function makeDirty(e){ if (!e.target.classList.contains('dirty')) e.target.classList.add('dirty'); } – Abir Stolov Aug 20 '18 at 20:45
  • Ideally I'd like CSS to allow :visited on form elements to accomplish this. – matty Jul 01 '20 at 11:31

13 Answers13

48

http://www.alistapart.com/articles/forward-thinking-form-validation/

Since we only want to denote that a field is invalid once it has focus, we use the focus pseudo-class to trigger the invalid styling. (Naturally, flagging all required fields as invalid from the start would be a poor design choice.)

Following this logic, your code would look something like this...

<style>
    input:focus:required:invalid {background-color: pink; color: white;}
    input:required:valid {background-color: white; color: black; }
</style>

Created a fiddle here: http://jsfiddle.net/tbERP/

As you'd guess, and as you'll see from the fiddle, this technique only shows the validation styling when the element has focus. As soon as you move focus off, the styling is dropped, regardless of whether it is valid or not. Not ideal by any means.

Carson
  • 6,105
  • 2
  • 37
  • 45
Bart
  • 6,694
  • 6
  • 43
  • 53
  • 19
    "Not ideal by any means." Indeed. Is there a way of applying styling to valid, but _not_ required content, after the element has lost focus? I haven't worked out a way of doing this. – Donald Jenkins Nov 28 '11 at 17:15
  • 1
    I could imagine a (not-currently-existent) pseduo-class for forms like "submitted" so that this would act as a parent selector for any contained inputs, like `#myform:submitted input:required:invalid`. Because even though this current scenario is unintuitive indeed, it isn't necessarily realistic to expect the CSS to know that a field is invalid under specific circumstances (why shouldn't it do it right away, as far as it knows?), so you're really wanting to tie the input style to the state of the form, not the input itself (eg, when required field is empty AND the parent form is submitted). – Anthony Jun 08 '17 at 22:14
  • This answer: https://stackoverflow.com/questions/41619497/how-to-make-pseudo-class-invalid-apply-to-an-input-after-submitting-a-form actually deals with this well, I think, even if it requires a bit of javascript. It just adds a class "submitted" to the form (just like I was suggesting!) so that the style rules can be `form.submitted input:required:invalid` – Anthony Jun 08 '17 at 22:18
  • Also, to totally exhaust this point, the same issue should be present with other `:valid` or `:invalid` input types. If an input is type `integer` and nothing is filled in, then it should render as invalid, since blank is not a valid numeric value. The only reason this isn't a problem is because outside of `required` property, an empty value is considered a valid value. – Anthony Jun 08 '17 at 22:21
  • when submit is clicked only 1 invalid field is highlighted at a time. Generally its convenient if all invalids are highlighted so users can correct them all once before hitting submit. is it better to remove required tag from all fields and then write custom js function for onsubmit – Aseem Oct 08 '18 at 00:45
44

These answers are out of date. Now you can do this by checking for a placeholder pseudo-class with CSS.

input:not(:placeholder-shown):invalid {
    background-color: salmon;
}
form:invalid button {
    background-color: salmon;
    pointer-events: none;
}
<form>
    <input type="email" placeholder="me@example.com" required>
    <button type="submit">Submit</button>
</form>

It starts with a normal background and turns pink as you enter you incomplete email address into it.

Carl
  • 1,039
  • 1
  • 9
  • 13
27

This is not possible in pure CSS, but can be done with JavaScript. This is a jQuery example:

// use $.fn.one here to fire the event only once.
$(':required').one('blur keydown', function() {
  console.log('touched', this);
  $(this).addClass('touched');
});
/**
 * All required inputs initially are yellow.
 */
:required {
  background-color: lightyellow;
}

/**
 * If a required input has been touched and is valid, it should be white.
 */
.touched:required:valid {
  background-color: white;
}

/**
 * If a required input has been touched and is invalid, it should be pink.
 */
.touched:required:invalid {
  background-color: pink;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p>
  <label>
    Name:
    <input type="text" required> *required
  </label>
</p>
<p>
  <label>Age:
    <input type="text">
  </label>
</p>
kzh
  • 19,810
  • 13
  • 73
  • 97
5

This is a VanillaJS (no jQuery) version of kzh's answer

{
  let f = function() {
    this.classList.add('touched')
  }
  document
    .querySelectorAll('input')
    .forEach((e) => {
      e.addEventListener('blur', f, false)
      e.addEventListener('keydown', f, false)
    })
}
/**
 * All required inputs initially are yellow.
 */
:required {
  background-color: lightyellow;
}

/**
 * If a required input has been touched and is valid, it should be white.
 */
.touched:required:valid {
  background-color: white;
}

/**
 * If a required input has been touched and is invalid, it should be pink.
 */
.touched:required:invalid {
  background-color: pink;
}
<p><label>
  Name:
  <input type="text" required> *required
</label></p>
<p><label>Age:
  <input type="text">
</label></p>
yunzen
  • 32,854
  • 11
  • 73
  • 106
  • 2
    While this is a great solution, you might want to swap `querySelectorAll` for `getElementsByTagName`, as it's about 17000 times (!) faster on my machine. Test it for yourself: https://jsperf.com/queryselectorall-vs-getelementsbytagname – Boltgolt Jul 13 '19 at 09:32
  • 1
    @Boltgolt General reminder: Premature optimization is always a bad idea. Especially with stuff that runs in the order of microseconds. Yes, querySelectorAll takes 38μs vs 9μs for getElementsByTagName, but that's just not going to make a difference when creating a form like this. – David Mulder Mar 01 '22 at 14:32
  • @DavidMulder i disagree in this case, why stick with a slightly worse solution when a faster solution is as easy as replacing `querySelectorAll` with `getElementsByTagName`? I personally think it's more about choosing the right tool for the job. – Boltgolt Mar 02 '22 at 18:17
4

Mozilla takes care of this with its own :-moz-ui-invalid pseudoclass that only applies to forms after they've been interacted with. MDN does not recommend using this due to a lack of support. However, you can modify it for Firefox.

There's a level 4 spec for a :user-invalid spec on the horizon that will offer similar behavior.

Michael M.
  • 10,486
  • 9
  • 18
  • 34
Aaron Benjamin
  • 1,291
  • 3
  • 18
  • 27
3

I created a small shim to deal with this in my codebase. I just start off with my <form/> element having the novalidate property along with a data-validate-on="blur" attribute. This watches for the first event of that type. This way you can still use the native :invalid css selectors for the form styling.

$(function () {
    $('[data-validate-on]').each(function () {
        var $form = $(this);
        var event_name = $form.data('validate-on');

        $form.one(event_name, ':input', function (event) {
            $form.removeAttr('novalidate');
        });
    });
});
aghouseh
  • 86
  • 4
2

There is a html5 invalid event that fires on form elements before the submit event occurs for each element that does not pass checkValidity. You can use this event to apply a class for example to the surrounding form and display :invalid styles only after this event occurs.

 $("form input, form select, form textarea").on("invalid", function() {
     $(this).closest('form').addClass('invalid');
 });

Your CSS would then look something like this:

:invalid { box-shadow: none; }
.invalid input:invalid,
.invalid textarea:invalid,
.invalid select:invalid { border: 1px solid #A90909 !important; background-color: #EEC2C2; }

The first line removes the default styling, so form elements look neutral at page load. As soon as the invalid event fires (when a user tries to submit the form), the elements are visibly rendered invalid.

Liquinaut
  • 3,759
  • 1
  • 21
  • 17
1

You could make it so that only elements that have a certain class on them and are required, are pink. Add an event handler to each required element that adds that class when you leave the element.

Something like:

<style>
  input.touched:invalid { background-color: pink; color: white; }
  input.touched:valid { background-color: white; color: black; }
</style>
<script>
  document.addEventListener('DOMContentLoaded', function() {
    var required = document.querySelectorAll('input:required');
    for (var i = 0; i < required.length; ++i) {
      (function(elem) {
        function removeClass(name) {
          if (elem.classList) elem.classList.remove(name);
          else
            elem.className = elem.className.replace(
              RegExp('(^|\\s)\\s*' + name + '(?:\\s+|$)'),
              function (match, leading) {return leading;}
          );
        }

        function addClass(name) {
          removeClass(name);
          if (elem.classList) elem.classList.add(name);
          else elem.className += ' ' + name;
        }

        // If you require a class, and you use JS to add it, you end up
        // not showing pink at all if JS is disabled.
        // One workaround is to have the class on all your elements anyway,
        // and remove it when you set up proper validation.
        // The main problem with that is that without JS, you see what you're
        // already seeing, and stuff looks hideous.
        // Unfortunately, you kinda have to pick one or the other.


        // Let non-blank elements stay "touched", if they are already,
        // so other stuff can make the element :invalid if need be
        if (elem.value == '') addClass('touched');

        elem.addEventListener('blur', function() {
          addClass('touched');
        });

        // Oh, and when the form submits, they need to know about everything
        if (elem.form) {
          elem.form.addEventListener('submit', function() {
            addClass('touched');
          });
        };
      })(required[i]);
    }
  });
</script>

And of course, it won't work as is in IE8 or below, as (a) DOMContentLoaded is relatively new and wasn't standard when IE8 came out, (b) IE8 uses attachEvent rather than the DOM-standard addEventListener, and (c) IE8 isn't going to care about :required anyway, as it doesn't technically support HTML 5.

cHao
  • 84,970
  • 20
  • 145
  • 172
1

While using HTML5 form validation, try to use the browser to detect for invalid submissions/fields, rather than re-inventing the wheel.

Listen for the invalid event to add a class of 'invalid' to your form. With the 'invalid' class added, you can go to town with styling your form using CSS3 :pseudo selectors.

For example:

// where myformid is the ID of your form
var myForm = document.forms.myformid;

var checkCustomValidity = function(field, msg) {
    if('setCustomValidity' in field) {
        field.setCustomValidity(msg);
    } else {
        field.validationMessage = msg;
    }
};

var validateForm = function() {

    // here, we're testing the field with an ID of 'name'
    checkCustomValidity(myForm.name, '');

    if(myForm.name.value.length < 4) {
        checkCustomValidity(
            // alerts fields error message response
            myForm.name, 'Please enter a valid Full Name, here.'
        );
    }
};

/* here, we are handling your question above, by adding an invalid
   class to the form if it returns invalid.  Below, you'll notice
   our attached listener for a form state of invalid */
var styleInvalidForm = function() {
    myForm.className = myForm.className += ' invalid';
}

myForm.addEventListener('input', validateForm, false);
myForm.addEventListener('keyup', validateForm, false);
myForm.addEventListener('invalid', styleInvalidForm, true);

Now, simply style your form as you see fit based on the 'invalid' class we've attached.

For example:

form.invalid input:invalid,
form.invalid textarea:invalid {
    background: rgba(255, 0, 0, .05);
    border-color: #ff6d6d;
    -webkit-box-shadow: 0 0 6px rgba(255, 0, 0, .35);
    box-shadow: 0 0 6px rgba(255, 0, 0, .35);
}
  • 7
    Er, isn’t this reinventing the wheel a heck of a lot more than the question? – Ry- Nov 09 '13 at 01:32
1

A good way is to abstract :invalid, :valid with a CSS classes and then some JavaScript to check if the input field was focused or not.

CSS:

input.dirty:invalid{ color: red; }
input.dirty:valid{ color: green; }

JS:

// Function to add class to target element
function makeDirty(e){
  e.target.classList.toggle('dirty');
}

// get form inputs
var inputs = document.forms[0].elements;

// bind events to all inputs
for(let input of inputs){
  input.addEventListener('invalid', makeDirty);
  input.addEventListener('blur', makeDirty);
  input.addEventListener('valid', makeDirty);
}

DEMO

Ikhlak S.
  • 8,578
  • 10
  • 57
  • 77
1

Following on from agouseh's idea, you can have a bit of javascript to tell when the submit button has been focussed, and have validation show up at that time.

The javascript will add a class (eg. submit-focussed) to the form field when the submit button is focussed or clicked, which then allows the CSS to style invalid inputs.

This follows the best practice of showing validation feedback after the user has finished filling in the fields, as according to research there is no additional benefit to showing it during the process.

document
  .querySelector('input[type=submit]')
  .onfocus = function() {
    this
      .closest('form')
      .classList
      .add('submit-focussed');
  };
form.submit-focussed input:invalid {
  border: thin solid red;
}
<form>
  <label>Email <input type="email" required="" /></label>
  <input type="submit" />
</form>

jQuery alternative

(function($) {
  $('input[type=submit]').on('focus', function() {
    $(this)
      .parent('form')
      .addClass('submit-focussed');
  });
})(jQuery); /* WordPress compatible */
robrecord
  • 504
  • 5
  • 15
0

Here is my method to avoid the default styling of any unfocused input as invalid, you just have to add a simple js command onFocus to let the webpage to identify focused and unfocused inputs, so all the input will not appear in the style of invalid at first place.

<style>
input.focused:required:invalid { background-color: pink; color: white; }
input:valid { background-color: white; color: black; }
</style>
…
<input name="foo" class="notfocused" onFocus="document.activeElement.className='focused';" required />

Try it yourself below:

input.focused:required:invalid {
  background-color: pink;
  color: white;
}

input:required:valid {
  background-color: darkseagreen;
  color: black;
}
<label>At least 1 charater:</label><br />
<input type="text" name="foo" class="notfocused" onFocus="document.activeElement.className='focused';" required />
timo
  • 95
  • 4
0

I can't comment, but to go with @Carl's very useful answer regarding using :not(:placeholder-shown). As another comment mentioned, this will still show the invalid state if you have NO placeholder (as some form designs call for).

To solve this, simply add an empty placeholder like so

<input type="text" name="username" placeholder=" " required>

Then your CSS, something like

:not(:placeholder-shown):invalid{ background-color: #ff000038; }

Worked for me!