Share options

Links to related pages

Essays about code. Essays about connection. Essays about context. Critiques of coding practices.

Letʼs enhance our forms

Start with workable HTML forms, then make them better.

Time to read

, 1563 words, 4th grade

In our previous article we showed how to create a form in HTML that validates input. And does so without using JavaScript or an external library.

Check out the example form from that article that validates input without JS.

Hiding and showing the password

Now, how can we use a bit of JavaScript to enhance the experience of our form? Well, one common enhancement is to add the ability to hide and show the password. So letʼs do that. To keep it simple, we'll strip out the other fields.

Our basic password field.
<div
	class="xx-form-field"
	id="xx-password-field"
>
	<label
		class="xx-field-label"
		for="xx-password-input"
		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="xx-password-input"
		name="password"
		pattern="[a-zA-Z]{4,}( [a-zA-Z]{4,}){3,}"
		required=""
		size="36"
		type="password"
	/>
</div>

This example shows an outer div element with CSS class “xx-form-field” and ID “xx-password-field”. This element encloses the field and has four child elements:

  1. A label element with CSS class “xx-field-label”, ID “xx-password-label,” and the for attribute set to the ID of the input element, “xx-password-input.”
  2. A br element to move the label above the input rather than to the left of it.
  3. A div element with CSS class “xx-field-help” and ID “xx-password-help.” This element contains help for the field.
  4. Last but not least, the input element itself. We set the aria-labelledby attribute to the IDs of the help text and the label. This means users of screen readers hear both.

    We give the input two CSS classes: “xx-field-input” — common to all inputs — and “xx-password-field” — unique to password inputs. And we set the ID to “xx-password-inpput.”

    The name attribute is set to “password”. We also set the required attribute to make the password a required field. We set the size attribute to 36 to set the width of the input, and set the type attribute to “password” to mask the input automatically.

    Finally, we set the pattern attribute to a regular expression that limits the input to four or more words of four or more characters length. Effectively, we want a passphrase, not a password.

We are going to assume that the above is pretty self-explanatory at this point. If not, read the previous article linked above.

The easiest way to do this is to add a button. When clicked, our button will toggle the password field type. Now password, now text. Easy peasy.

All we need to do is toggle between password and text.
globalThis.addEventListener("DOMContentLoaded", function () {
	const field = document.querySelector("#xx-password-field")
	const label = field.querySelector("label")
	const input = field.querySelector("input")
	const button = document.createElement("button")

	button.type = "button"
	button.classList.add("xx-toggle-password")
	button.innerText = "show"
	button.setAttribute("aria-label", "Show password.")

	button.addEventListener("click", () => {
		if (input.type === "password") {
			input.type = "text"
			button.innerText = "hide"
			button.setAttribute("aria-label", "Hide password.")
			return
		}

		input.type = "password"
		button.innerText = "show"
		button.setAttribute("aria-label", "Show password.")
	})

	label.appendChild(button)
})

This example shows the JavaScript needed to provide show and hide functionality to the password field. It is described in the text below.

Letʼs go through this step by step. Itʼs pretty simple.

  1. We begin by adding an event listener to our globalThis object to run on “DOMContentLoaded” (line #1). We pass it an anonymous arrow function that will add the enhancement to the password field.
  2. In our arrow function, we begin by setting four const variables (lines #2 to #5):
    1. We use document.querySelector to get the outer div element of our field using its ID. We assign this to “field.”
    2. Using the query selector on the field itself, we get the label and input elements by tag name. We assign them to “label” and “input” variables, respectively.
    3. Finally, we use document.createElement("button") to create a button element. We assign it to a variable with name, “button.” Note our careful naming to make it easy to understand our code.
  3. Now we want to configure our button. As this is different from declaring and assigning variables, we leave a blank line. This makes the separation of concerns obvious. Then, on lines #7 to #10, we:
    1. Set the button type to “button” to prevent it from submitting our form when activated. Kind of important, no?
    2. Use button.classList.add("xx-toggle-password") to add a CSS class to the button. This makes it easy to style it.
    3. Set button.innerText to label the button, “show.”
    4. Finally, set an aria-label on the button to “show password.” This makes the buttonʼs function clearer to users of screen readers.
  4. Now we have our button. We want to add an event listener to toggle the password field type when the user clicks the button. Weʼll leave another blank line. Then on lines #12 to #23, we will create our event handler as an anonymous arrow function. We assign it to the buttonʼs click event.
    1. Line #12 uses button.addEventListener("click", ...) to assign the arrow function to the buttonʼ click event.
    2. In our arrow function, we will need to handle two conditions: one when we have masked the input and one where we havenʼt. The safest way to test this is by checking the type of the input. Ergo, on line #13, we check that the input type is “password,” type === "password", our default state.
    3. If the condition returns true (type is “password”), then we:
      1. set the input type to “text” on line #13,
      2. set the buttonʼs innerText to “hide” on line #14,
      3. and set the aria-label to “hide password” on line #15.
    4. Then we return from the function. The password should now be visible and the button should say, “HIDE.”
    5. If the condition fails, i.e., the input type isnʼt “password,” then we have unmasked the field. So we:
      1. set the type back to “password” (line #18)
      2. set the buttonʼs innerText back to “show”
      3. and of course, our aria-label back to “show password.”
      And thereʼs our toggle.
  5. Last but not least, we add our button to the form on line #22 with label.appendChild(button). As a screen reader reads our label aloud, so, too, will it read the buttonʼs aria-label.

As we say, easy peasy.

Why not a component library?

“But … but … but …” we can hear some readers say, “why not just use a pre-coded field from a popular component library?”

And, “why would we want to have to write this code every time we wanted an enhanced password field?”

Why on Earth would we do that? Or on Mars, for that matter?

I put this field together once. I use Astro for its bundling benefits: it makes a component architecture easy. I have no need for its “islands” architecture. So, I have been building my own component library bit by bit (pun intended).

Although I shun passwords as much as practicable, it pays to have a field like this. So I added a PasswordField to my Astro component library.

And I included a prop called allowShow. When it is true, then the component includes the JS enhance script and features a “SHOW” button. When it is false or missing, the component does not.

Easy peasy, as I may have said once or twice before. And the benefit is obvious:

I know every line of the code.

I wrote it, and the responsibility for making sure it is bug-free, accessible, user-friendly, etc. is on me. I am not counting on devs I have never met to keep my codebase current, robust, and secure.

And because it is an ad hoc component — written specifically to fill my needs — rather than a highly-abstracted, one-size-fits-all attempt to please everyone, it can be small and efficient. No superfluous parts. William of Ockham would be proud.

Case in point:

All this for a simple password field? Really?
<div class="MuiFormControl-root css-kzv9dm">
	<label
		class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-sizeMedium MuiInputLabel-filled MuiFormLabel-colorPrimary MuiFormLabel-filled MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-sizeMedium MuiInputLabel-filled css-1rgmeur"
		data-shrink="true"
		for="filled-adornment-password"
	>
		Password
	</label>
	<div class="MuiInputBase-root MuiFilledInput-root MuiFilledInput-underline MuiInputBase-colorPrimary MuiInputBase-formControl MuiInputBase-adornedEnd css-1thjcug">
		<input
			aria-invalid="false"
			id="filled-adornment-password"
			type="password"
			class="MuiInputBase-input MuiFilledInput-input MuiInputBase-inputAdornedEnd css-ftr4jk"
			value=""
		/>
		<div class="MuiInputAdornment-root MuiInputAdornment-positionEnd MuiInputAdornment-filled MuiInputAdornment-sizeMedium css-1mzf9i9">
			<button
				class="MuiButtonBase-root MuiIconButton-root MuiIconButton-edgeEnd MuiIconButton-sizeMedium css-slyssw"
				tabindex="0"
				type="button"
				aria-label="toggle password visibility"
			>
				<svg
					class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-vubbuv"
					focusable="false"
					aria-hidden="true"
					viewBox="0 0 24 24"
					data-testid="VisibilityIcon"
				>
					<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"></path>
				</svg>
				<span class="MuiTouchRipple-root css-w0pj6f"></span>
			</button>
		</div>
	</div>
</div>

This example shows the HTML output of a password component from the MUI component library. There is an outer div element. This encloses a label element and another div element.

The inner div element contains an input element (the password input) and yet another div element. That innermost div element contains a button element which contains an svg icon of an eye.

The label has a data-shrink attribute, most likely used to cue the CSS/JS to use the Material Design placeholder-shrinks-to-tiny-label design. The input element has an aria-invalide attribute set to “false.”

The button has its tabindex set to zero, pointlessly. It also has an aria-label of “toggle password visibility.” Finally, the svg element has focusable set to false and aria-hidden set to true.

There is also an empty span element with class name “MuiTouchRipple” indicating that it is used solely for a visual “ripple” effect.

To be fair, the MUI folks have improved their code in many ways over the past several years. Now they use the proper data- attribute format. They have discovered aria- attributes as well.

They even have an aria-label attribute. That said, “toggle password visibility” is wordy and fails to make clear the current state of the field. Is it unmasked or masked?

There remain several UX problems, but weʼll save those for another article.

What is most noticeable about the above code is the excessive number of CSS class names. Why on Earth do we need so many?

Well, here is the root of the problem. MUI must abstract beyond belief so that anyone can use it for anything. This is not an MUI problem; it is a component library problem.

The irony is that MUI was once based on Material Design, and still is to some degree. But they have wrangled it to the point that you could make it look like an old Windows 95 form if you wanted to.

But why? My sites each have a specific look and feel. I have a design system for each. I do not need an enormous stylesheet and a million classnames to make them look and work in different ways. Ways that I will never need.

I give each relevant HTML element one class — maybe two. Then I write my stylesheet to set the specific style for my design system.

It gets worse

Oh, but thereʼs more.

Where is the CSS for this field? Where is the JavaScript?

Why, they have bundled them away somewhere else. Good luck finding them. Good luck changing them.

Instead, MUI provides a complex system for configuring their components. Hey, you are no longer a programmer or even a developer. You are now a configurer.

Fun, right?

If you want this field to work some way that the MUI folks didnʼt allow for, then good luck. I have spent many painful hours trying to force MUI to fit some designerʼs style preference. Oh, the PTSD!

But when we code the field ourselves with simple HTML, CSS, and JavaScript, then it is all right there.

A recent fad — devs can never leave well enough alone — is something called LoB, Locality of Behavior. LoB states that the behavior of a unit of code should be as obvious as possible by looking only at that unit of code.

I guess multiple screens and the ability to put several files side-by-side was the wrong path. Silly me.

But with my Astro component model, I could, if I wanted to (I donʼt), create a Single-File Component (SFC). That would put the CSS, JS, and HTML all in the same “PasswordField.astro” file.

Me? I prefer to keep structure/semantics, style/layout, and behavior separate. In separate files. And with roughly 10ha of screen real estate at my workstation, I can refer to side-by-side files quickly as I work.

So my Astro PasswordField component has a folder called “PasswordField” and three files:

PasswordField/
  • index.astro
  • index.css
  • index.ts

I import the CSS and TypeScript files at the top of my Astro file. Astro builds the component, transpiles the TS to JS, and bundles and dedupes all the CSS and JS.

We can do a lot more with simple JavaScript enhancements. But it might surprise you how little we need to do so.

In a coming article, we will show how many HTML form components are already quite enhanced enough.

Links to related pages

Essays about code. Essays about connection. Essays about context. Critiques of coding practices.

Get notified form

Get notified of site updates
Button bar

Carbon emissions for this page

Cleaner than 99% of pages tested
0.016g on first visit; then on return visits 0.010g
QR Code

Scan this code to open this page on another device.