HTML can validate fields for you: required, type="email", minlength, pattern. CSS can then show the result.
:valid and :invalid
<style>
input { padding: 9px 12px; border: 2px solid #b9b2ad; border-radius: 6px; font: inherit; }
input:valid { border-color: #27ae60; }
input:invalid { border-color: #c0392b; }
</style>
<input type="email" placeholder="you@example.com" required>
Try it: the empty required field is red straight away, before you have typed anything. That is a poor experience, and it is the main problem with :invalid.
Wait until the user has interacted: :user-invalid
:user-invalid and :user-valid apply only after the user has interacted with the field. This is how it should have worked in the first place, and current browsers support it.
<style>
input { padding: 9px 12px; border: 2px solid #b9b2ad; border-radius: 6px; font: inherit; }
input:user-invalid { border-color: #c0392b; background: #fdf2f2; }
input:user-valid { border-color: #27ae60; }
</style>
<input type="email" placeholder="Type, then click away" required>
Type something that is not an email and then click elsewhere to see the error state.
Do not use color alone
Always add a text message, and link it to the field so screen readers read it.
<label for="e">Email</label>
<input id="e" type="email" required aria-describedby="e-help">
<p id="e-help" class="error">Enter a valid email address, for example name@example.com.</p>
.error { color: #b00020; font-size: 0.875rem; }
.error::before { content: "⚠ "; }
The icon and text mean people who cannot see red still get the message.