DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Six inputs on our four public forms, zero autocomplete attributes, and the only one in the app says off

Every page in PubTrivia's sitemap is prose. 79 URLs, and not one of them contains a <form>:

$ for p in $(curl -s https://pub-trivia.app/sitemap.xml | grep -oE '<loc>[^<]*' | sed 's|<loc>https://pub-trivia.app||'); do
    curl -s "https://pub-trivia.app$p" | grep -oic '<form'
  done | paste -sd+ - | bc
0
Enter fullscreen mode Exit fullscreen mode

The forms are on four pages that are deliberately not in the sitemap, because a sign-in box is not a page you want ranked: /login, /signup, /forgot-password and /unsubscribe. Between them they hold six inputs a human types into.

They have zero autocomplete attributes. Not a wrong one, not a lazy off: the string does not appear in the HTML of any of the four pages, and it appears exactly once in the whole application's source, which I will come back to.

What is actually there

Pulled out of the live HTML, the six are:

<!-- /login -->
<input type="email"    id="email"    name="email"    required placeholder="m@example.com">
<input type="password" id="password" name="password" required>

<!-- /signup -->
<input type="text"     id="name"     name="name"     required placeholder="John Doe">
<input type="email"    id="email"    name="email"    required placeholder="m@example.com">
<input type="password" id="password" name="password" required>

<!-- /forgot-password -->
<input type="email"    id="email"    name="email"    required placeholder="m@example.com">
Enter fullscreen mode Exit fullscreen mode

The parts that are right are right. Correct input types, so a phone shows an email keyboard and a password field is masked. required, so the browser refuses an empty submit without a round trip. Real labels, properly associated, which is the thing people usually get wrong:

<label for="email">Email</label>
<label for="password">Password</label>
Enter fullscreen mode Exit fullscreen mode

The missing attribute is the one that tells the browser what a field is for rather than what shape it is.

The three things the omission costs

One: it is a WCAG failure, not a nicety. Success criterion 1.3.5, Identify Input Purpose, is Level AA, and it asks for an autocomplete token on any field collecting information about the user where the purpose is in its defined list. Name, email, current password and new password are all in that list, which covers all six of our inputs. So every input on all four pages fails it, and the remediation is six attributes with four distinct values.

Two: on the signup form it changes behaviour, not just compliance. This is the one that matters in practice. A password field with no token is treated by browsers and password managers as a field for a password you already have. With autocomplete="new-password", the same field is a field for a password you are about to invent, and that single token is what triggers the strong-password suggestion in Safari and iOS, and what stops a manager helpfully filling the password you use on the previous page. Our signup and sign-in password inputs are byte-identical, so nothing can tell them apart except the heuristics guessing from the surrounding page.

Three: everything else is luck, and it currently holds. Chrome and Safari do not need the attribute to recognise a field called email with type="email", and they fill it. I checked that they do. The point is that it is a guess based on English attribute names, and it is a guess that gets re-evaluated with every browser release, on a form where getting it wrong means a customer cannot sign in.

The id that is itself an autocomplete token

There is a small extra edge here. Three of the six inputs carry an identifier that is itself a defined autocomplete token: id="name" once and id="email" twice. Chrome's DevTools raises an issue for exactly that shape, FormInputAssignedAutocompleteValueToIdOrNameAttributeError, and when I opened DevTools on every page of this site a while ago, the sign-in page was the only one that reported it.

The warning sounds pedantic and is not. The browser's heuristics read the id and name attributes to guess purpose; an element whose identifier is a token, while carrying no token, is the one case where the two signals say different things. The fix is the same four attributes: once purpose is declared, nothing has to be inferred from an identifier.

The only autocomplete we ever wrote is an off

Here is the single occurrence in the entire source tree:

$ grep -rn autoComplete app components --include='*.tsx'
components/PlayerLobby.tsx:333:  autoComplete="off"
Enter fullscreen mode Exit fullscreen mode

That is the field a player types a name into on their own phone at a pub table. And it is the one place in this product where turning autofill off is the right call, which is a pleasing accident. A phone handed round a table of six, a name that is a joke rather than an identity, and a browser that would otherwise offer the last person's entry: that is a field where remembering is the wrong behaviour.

So the state of the codebase is that the one form we consciously thought about autofill on is the one where we switched it off, and the four where a customer is signing into a paid product were left to the browser's judgement. That is not a conclusion about autofill. It is a conclusion about what gets attention: the player's phone is the surface we demo, and a sign-in box is furniture.

What the framework put in those forms, for contrast

While counting attributes I noticed how much of each form is not ours. Every one of these forms posts to a Server Action, and the framework serialises the call into hidden inputs, including the action's initial state:

<input type="hidden" name="$ACTION_REF_2">
<input type="hidden" name="$ACTION_2:0" value='{"id":"607d152f2d80...","bound":"$@1"}'>
<input type="hidden" name="$ACTION_2:1" value='[{"message":""}]'>
<input type="hidden" name="$ACTION_KEY" value="k87068aacb86963be7edcebcf76df16bd">
Enter fullscreen mode Exit fullscreen mode

That [{"message":""}] is the initial state of the useActionState hook, in the page, as a form field, so that a submit without JavaScript arrives with the same arguments the client would have sent. The sign-up page's version is [{"message":"","success":false}], and the unsubscribe page's is [{"status":"idle"}], so the shape of each action's state type is visible from outside. None of it is secret, and it is a good reminder of how progressive enhancement is actually implemented: four hidden fields per form, written by the framework, next to six attributes that we did not write.

The fix, in full

- <input type="text" id="name" name="name" required />
+ <input type="text" id="name" name="name" required autoComplete="name" />

- <input type="email" id="email" name="email" required />
+ <input type="email" id="email" name="email" required autoComplete="email" />

  // sign-in
- <input type="password" id="password" name="password" required />
+ <input type="password" id="password" name="password" required autoComplete="current-password" />

  // sign-up
- <input type="password" id="password" name="password" required />
+ <input type="password" id="password" name="password" required autoComplete="new-password" />
Enter fullscreen mode Exit fullscreen mode

Four tokens, one of which has to differ between two visually identical fields on two different pages, which is precisely why no heuristic can be relied on to get it right.

Check it yourself

The count is one command per page:

$ for p in /login /signup /forgot-password /unsubscribe; do
    printf '%s %s\n' "$p" "$(curl -s https://pub-trivia.app$p | grep -oic autocomplete)"
  done
/login 0
/signup 0
/forgot-password 0
/unsubscribe 0
Enter fullscreen mode Exit fullscreen mode

The behavioural half needs a browser. Open the sign-up page with a password manager enabled and see what it offers in the password field, then compare it with the sign-in page. They are the same input, and in a correctly marked up form they would not be. The free tier needs no card, so you can go all the way through and see which of your tools guessed right.

Top comments (0)