I'm creating an address form for my web application and am having trouble figuring out the styling rules for HTML5 form validation with the required attribute. The examples and behavior described below are using Firefox.
The HTML for the first input field of the form looks like this:
<label for="addressLine1" class="form__label">
Address Line 1
</label>
<input type="text" required aria-required="true" class="form__input-text" id="addressLine1"/>
Without any custom styling the input behaves like this:


I want to retain this behavior, where the input displays some default styling on load, and only displays "invalid" styling if the user tries to submit the form with any required fields blank (or otherwise invalid). But I can't find a straight answer as to what attributes/pseudo classes I need to modify to change the styling while retaining this behavior. If I use the :invalid pseudo class, I get this behavior:



How do you retain the default behavior (default styling on load, invalid styling on invalid submission) with custom styles, and can it be done with just CSS or do I have to add some JS functionality?
Alright so after reading over the CSS Pseudo Class docs on MDN, it doesn't look like there is any combination of pseudo classes you can string together to model the various states that make this behavior work correctly. So after playing around a bit and looking over the Bootstrap validation link Alex Schaeffer suggested, but deciding I didn't want to add extra dependencies/style sheets I didn't really need, here's the solution I came up with that adds minimal extra CSS and JavaScript.
First off, the red border was, indeed, a box shadow, so I was able to override that just by adding this to my (S)CSS:
.form__input-text {
/* default input styling goes here */
box-shadow: none;
}
Next, I added a bit of state to my component to keep track of whether or not the form has been validated yet. I'm using Svelte, so this was as simple as adding a boolean variable inside the component's <script> tag like so:
let wasValidated = false;
Then I added a conditional class to my HTML/JSX. If you're using another framework or jQuery/vanilla JS, you might need to explicitly do this with a function wired to an event handler, but in Svelte I just need to change my markup to this:
<label for="addressLine1" class="form__label">
Address Line 1
</label>
<input
type="text"
required aria-required="true"
class="form__input-text"
class:wasValidated="{wasValidated}"
id="addressLine1"
/>
All the class:wasValidated="{wasValidated}" bit is doing is conditionally adding a .wasValidated class to that input element if/when the wasValidated variable is truthy.
Then, back in my (S)CSS I added the following to apply my "invalid" styling (which at this point just changes to border color to a shade of red) only when the form had been validated at least once, and only to invalid elements:
input.wasValidated:invalid {
border-color: $red;
}
Then I wired a simple onClick function to the submit button that changes the wasValidated variable to true when the button is clicked:
HTML/JSX
<button on:click|preventDefault={onClick} class="form__submit-button" type="submit">
Search
</button>
JS
const onClick = e => {
wasValidated = true;
};
The function needs to be wired to a click event and not a submit event, because the submit event is never triggered if the form fails validation.
So now, when the page first loads, all the form inputs display the default styling, regardless of validity, because wasValidated is set to false. Then, when the submit button is clicked wasValidated is toggled to true, the .wasValidated class is applied to any required elements, which, if they are invalid, then display the "invalid" styling. Otherwise, if the form is successfully submitted, the onSubmit function wired to the form handles things from there.
Edit: As it turns out, in Svelte, you can unbind event handlers after the first time the event fires. So my markup for the submit button now looks like this:
<button on:click|preventDefault|once={onClick} class="form__submit-button" type="submit">
Search
</button>
Adding the |once modifier to on:click unbinds the onClick function the first time the button is clicked, so the function doesn't keep firing unnecessarily if the user attempts to submit invalid data multiple times.
How do you retain the default behavior (default styling on load, invalid styling on invalid submission) with custom styles, and can it be done with just CSS or do I have to add some JS functionality?
You can achieve this effect with a very small amount of javascript (four lines).
The reason why your input is showing as invalid is because it is both empty and required.
So one 3-step approach looks like this:
Step 1: Declare the element in your HTML using the attribute required
<input type="text" required>
Step 2: Then remove that attribute via javascript immediately
const addressLine1Input = document.getElementById('addressLine1');
addressLine1Input.removeAttribute('required');
Step 3: Then, as soon as a single character is entered into the <input> use javascript a second time to add the required attribute back in again.
const setRequired = (e) => e.target.required = 'required';
addressLine1Input.addEventListener('keyup', setRequired, false);
You can test that all this is working below by adding one or several characters to the <input> and then deleting all of them.
You will see that the <input> is initially empty but does not show as invalid, then contains characters and does not show as invalid and, finally, is empty again and now does show as invalid.
Working Example:
const addressLine1Input = document.getElementById('addressLine1');
addressLine1Input.removeAttribute('required');
const setRequired = (e) => e.target.required = 'required';
addressLine1Input.addEventListener('keyup', setRequired, false);
input:invalid {
background-color: rgba(255, 0, 0, 0.3);
border: 2px solid rgba(255, 0, 0, 0.5);
}
<form>
<label for="addressLine1" class="form__label">Address Line 1</label>
<input type="text" name="addressLine1" id="addressLine1" class="form__input-text" placeholder="Enter address here..." aria-required="true" required>
</form>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With