You can validate your forms without JavaScript.
Less than 6 minutes, 1470 words, 4th grade
It is easy to create web forms that validate themselves without JavaScript. Why so many developers ignore this native capability is an open question.
I guess that when all you have is React or similar, then everything looks like a React component.
But what happens when the user disables JavaScript (or it is otherwise unavailable)? OK, you are rendering your React components on the server. Nice. But what happens to your client-side validation?
But before we get into this, a brief digression. Letʼs take another look at our previous article, Progressive enhancement.
The requestAnimationFrame
option
After I published that article, I got a suggestion from a good friend. He recommended that I animate the accordion elements with requestAnimationFrame
. So I tried it:
function toggleAccordion (event) {
event.preventDefault()
const summary = event.target
const accordion = summary.closest("details")
const content =
accordion.querySelector(".xx-accordion-content")
const openHeight = summary.xxOpenHeight
if (accordion.open) {
function shutAccordion (
height,
stopHeight = 0,
decrement = 20
) {
return function () {
content.style.maxHeight = `${height}px`
if (height <= stopHeight) {
accordion.open = false
return
}
requestAnimationFrame(
shutAccordion(
height - decrement,
stopHeight,
decrement
)
)
}
}
shutAccordion(openHeight)()
return
}
function openAccordion (
height,
stopHeight,
increment = 10
) {
return function () {
content.style.maxHeight = `${height}px`
if (height >= stopHeight) {
return
}
requestAnimationFrame(
openAccordion(
height + increment,
stopHeight,
increment
)
)
}
}
accordion.open = true
openAccordion(0, openHeight)()
}
I wonʼt go into a lot of detail here as it is pretty self-explanatory. We create open and close outer functions that return an inner closure. requestAnimationFrame
calls that closure recursively. In this way, we can increment or decrement the height on each frame immutably.
Is this a potential performance bottleneck, creating all those closures? Maybe. But itʼs the simplest way to do it, and we donʼt prematurely optimize here at Craft Code. If it turns out to be a bottleneck, weʼll switch to a loop and a mutable variable.
Betcha it works just fine.
But which is better? setTimeout
? Or requestAnimationFrame
?
Itʼs a pretty close call. setTimeout
is older and will work in browsers such as Internet Explorer pre-v10. Of course we can polyfill that.
An argument for requestAnimationFrame
is that it may be smoother. Especially if there are many simultaneous animations on the page. It also may be more performant. Donʼt need to support very old browsers? Then requestAnimationFrame
is probably the better way to go.
You should ask, “Where in the world are my users? What browsers are they using?” That will help you to determine which option is best. But then we should always start with that question, right?
Try it on our example animation frame accordion.
About those forms
Letʼs get to our forms. A few simple recommendations to start.
- Donʼt use placeholders. Just donʼt. Use
<label>
instead. Set thefor
attribute to theid
of the input labelled. - Group related controls in
<fieldset>
elements. Use the<legend>
element to label the group. Fieldsets are easily styled with CSS these days. - Ensure that your form elements are keyboard navigable. And in the same order that they appear visually. Donʼt confuse your users.
- You can best determine other considerations, such as where to put buttons or which buttons to use, with user testing. There is no one right way.
With those caveats in mind, let us begin with a simple form. Here is one with which we are all familiar … but not for much longer, we hope.
<form
action=""
class="xx-form"
method="GET"
name="simple-form"
>
<fieldset class="xx-fieldset">
<legend>Please sign in</legend>
<div class="xx-form-field">
<label
class="xx-field-label"
for="email"
id="xx-email-label"
>
Email address
</label>
<br>
<div class="xx-field-help" id="xx-email-help">
The email address with which you signed up.
</div>
<input
aria-labelledby="xx-email-help xx-email-label"
class="xx-field-input xx-email-field"
id="email"
name="email"
required
size="36"
type="email"
>
</div>
<div class="xx-form-field">
<label
class="xx-field-label"
for="password"
id="xx-password-label"
>
Password
</label>
<br>
<div class="xx-field-help" id="xx-password-help">
Four or more space-separated words of 4+ characters.
</div>
<input
aria-labelledby="xx-password-help xx-password-label"
class="xx-field-input xx-password-field"
id="password"
name="password"
pattern="[a-zA-Z]{4,}( [a-zA-Z]{4,}){3,}"
required
size="36"
type="password"
>
</div>
</fieldset>
<button
class="xx-submit-button"
type="submit"
>
Sign in
</button>
</form>
As in previous examples, we use CSS class names on all our elements. The xx-
is a namespace, i.e., cc-
for Craft Code. This allows us to select our elements in our CSS and avoids collisions.
We like grouping controls in fieldsets, and using the legend
for the title of our form. We group our labels and inputs in <div>
elements with the class name, xx-form-field
. This permits us to style them as a group.
Our view is that best practice is to put the label above the input. To make this work even when the user disables CSS, we use a <br>
element. Note that we tie our labels to their inputs with the for
attribute.
Whenever possible, we choose our HTML elements to take advantage of browser features. We chose an <input>
of type email
rather than type text
. Because of this, the browser will validate the value of the input on submit.
If the value is not a potentially valid email address, then submission fails.
We also set the required
attribute on the input. The form will require a valid email address before we can submit it.
One down.
Then for the password field, we choose the password
type, which masks the characters as we type them. We also set the required
attribute so the user must provide a password. We could use minlength
to set a minimum length for the password, but we have a better idea.
As this brilliant xkcd comic makes clear, a better approach to passwords is to use four random words. To this end, we have used the pattern
attribute on the password input. It requires the password to be four or more space-separated words of four or more characters each.
The pattern: [a-zA-Z]{4,}( [a-zA-Z]{4,}){3,}
.
Of course, we make this requirement clear with a help message above the input. There a screen reader will announce it before entering the field. And to be extra certain, we associate it with the input via the aria-labelledby
and id
attributes as shown.
Hereʼs what that looks like:
Here are the possibilities:
- Empty input (required): “Please fill out this field.”
- bob@: “Please enter a part following '@'. 'bob@' is incomplete.”
- @dobbs: “Please enter a part followed by '@'. '@dobbs' is incomplete.”
- bob.dobbs: “Please include an '@' in the email address. 'bob.dobbs' is missing an '@'.” (As above.)
Here is what happens if the password doesnʼt match our pattern:
Here are the possibilities:
- Empty input (required): “Please fill out this field.”
- Bob is yer uncle: “Please match the requested format.”
Be sure that you explain what the “requested format” is! Donʼt make your users guess!
Try “correct horse battery staple”. Does it work? xkcd would be proud.
Give this simple form a try.
What else can we validate?
Different types of input provide different validations. Here are some examples:
-
Pattern mismatch: You can use the
pattern
attribute with other input types as well. These include:email
,search
,tel
,text
, andurl
. -
Range overflow/underflow: You can set
max
andmin
values on typesdate
,datetime-local
,month
,number
,range
,time
, andweek
. -
Step mismatch: You can set the
step
value (a number) on typesdate
,datetime-local
,month
,number
,range
,time
, andweek
. Sheesh! Who knew there were so many input types? MDN provides a handy list of default values. -
Too long/short: You can set a
maxlength
and/orminlength
(as a number of characters) on typesemail
,password
,search
,tel
,text
, andurl
.
Donʼt try to memorize these. Remember: code (and learn) just in time. There is no point in wasting time and effort that you may never need.
Instead, first design your form. Then determine what needs validation. Finally, refer to Mozilla Developer Network or equivalent to see what fits your needs.
For example, you might create an integer input like this:
<input
id="iq"
max="210"
min="0"
name="iq"
required
step="1"
type="number"
>
This accepts only positive integers between 0 and 210 (the highest IQ on record). The step
value of 1
does not prevent entering decimals, such as 100.1. But try submitting that. Youʼll get a warning:
“Please enter a valid value. The two nearest valid values are 100 and 101.”
You can try it on our example form. What happens if you enter -50? What about 300?
You could also do this with an input of type text
by setting the pattern
attribute to [0-9]*
. Or use ([0-9]|[1-9][0-9]*)
? if you want to disallow starting zeros. But either way you lose the semantic value of the number
type.
Our recommendation: stick with the number
input.
The key takeaway here is this: good enough is, by definition, good enough.
Too often, designers and devs create bloated, ugly, brittle, incomprehensible code. We do it because we want to tweak our interface in some minor way, but plain HTML doesnʼt make it easy. So we hack the heck out of it.
The temptation to throw out all the benefits of semantic, accessible, browser-native code just to make it a bit slicker can be overwhelming. But resist, resist, resist!
This isnʼt about UX. Your users donʼt care about your flashy interface, no matter what they tell you when you ask. Thatʼs your ego talking.
Users care about usability, findability, comprehensibility, accessibility.
Can they find what they are looking for? Can they understand it when they find it? Can they make your site do what they want it to do?
And we can manage all that with an elegant design without having to hack the code. Simple and beautiful. And more stable, less likely to be buggy, and be easier to code and refactor, too.
And if they never see your flashy, edgy new design, they will never miss it.
Top comments (0)