How to fix HTML form inputs

UIScript DevBlog, Part 17

Bence Meszaros
8 min readOct 29, 2024
tl;dr (well, kinda)

Intro

HTML is a mess.

I’ve said this numerous times already, but this language is truly a gift that just keeps on giving.

In HTML, working with text is a mess, building tables is a mess, and yes, building a fucking form is a complete mess.

I cannot wrap my head around the fact that this “tech” has been around for 30+ years already and we still haven’t been able to figure out why we need an <input type="button"> when we already have the <button> element, or why <textarea> and <select> aren’t input types when they clearly input values, or why button is an input type when it clearly doesn’t input anything but instead performs an action.

What exactly is semantic about having a hidden input field, or better yet, an <input type="image"> that is a button, that is an image?

Yeah, I know, I am probably too dumb to understand that the best way to separate semantics, presentation and functionality is to mix them together as much as possible.

Skill issue, I know. I promise I will double my daily self-flagellation with the HTML specs.

But here is the thing. Forms are actually pretty easy. The hard part is the language that’s actively working against you.

Let me explain what I mean by that.

Types

The concept of data types is the cornerstone of programming. It doesn’t matter if the language is strongly or weakly typed, or if it uses static or dynamic types, as long as it has a formal definition of this concept.

The problem is, the HTML specs define a lot of shit from content models to tags and semantics, just not proper types.

Think about it. Why do you need a form?

To ask the user for a boolean, a number, a string, an array or an object.

That’s it.

You can twist and turn this however you want, use shiny abstractions, create a metric ton of obscure concepts and specifications, but at the end of the day, underneath of it all, you are just working with simple data types.

Once you understand this, all the random pieces suddenly fall into place.

Let’s see how.

Boolean

The simplest value we can get from the user is a boolean. It can be true or false, enabled or disabled, yes or no, on or off or whatever value pair you can come up with.

In HTML, we can use a checkbox for this:

<input type="checkbox">

Number

Numbers are a little bit trickier, because we usually represent them as strings: 0b1000 (binary), 42.5 (decimal), 1e3 (decimal exponential), 0o644 (octal), 0xFFFF (hexadecimal) and so on. But make no mistake, these are handled and stored as numbers under the hood (or at least they should be).

Note: A good way to distinguish a number from a string is to check whether it works in an arithmetic operation. For example, a hexadecimal number might be incremented but we’ll never do arithmetic operations on a phone number or a street address.

Another way to represent numbers is with a slider.

HTML actually supports them both:

<input type="number">
<input type="range">

String

Arguably the most chaotic in HTML is to get a string.

I can understand that HTML struggles with other data types, but I always find it hilarious when it fails spectacularly to work with text, even though literally everything is text/string in HTML.

In HTML, the following elements all expect a string input:

<input type="text">
<input type="email">
<input type="password">
<input type="search">
<input type="tel">
<input type="url">
<textarea>

It is a complete mystery to me why <textarea> isn’t an input element and even more so why all the other types aren’t simply the modifications of a single text input element. They differ only in presentation and/or data validation — features that have nothing to do with semantics at all.

Object

Then, there are situations where we need multiple pieces of data together. In these cases, we are talking about an object.

There are virtually endless objects we can ask from the user but the most common ones are date/time, color and file. Unfortunately, these are also pretty chaotic.

<input type="color">
<input type="date">
<input type="datetime-local">
<input type="time">
<input type="month">
<input type="week">
<input type="file">
  • First, the simple datetime type is obsolete, because it used UTC that didn’t really make much sense. But it is unclear whether time is UTC or not, I would assume that time would be UTC and time-local would be local following the same logic, but I don’t think this is the case.
  • Second, month is actually month and year, and week is also week and year, they just have incorrect names. Pretty annoying, but this is the best all of humanity can offer. Also, there is no day type because why would there be, right?
  • And third, these fields are designed to gracefully degrade into a single text input in unsupported browsers. This is a nice concept in theory, but completely useless in reality. The problem is, these are objects and serializing them into a string is ambiguous. How can I consume a date string for example, when I don’t even know what format it is using?

Also, week doesn’t even degrade into a text field on iOS (tested on iOS 18), it is just fully broken.

Note: If you are an eager beaver, you might have noticed that urls, email addresses and even phone numbers are objects, too. This is why it is difficult to validate them as strings, but at least they have much less formatting options than for example a date or a color. And they don’t have a non-degraded mode, so I classify them as text inputs.

Enum

This data type is less common but very simple: it defines a list of possible values to choose from. It is extremely useful in UI/UX situations, because instead of asking the user for, then trying to validate a raw value, we present a list of valid options to choose from right away.

HTML provides two implementations for this that are functionally equivalent:

<select>
<input type="radio">

Wait, what? These are the same?

That’s right, take a look at this:

<!-- this is the select tree -->
<select size="3">
<option value="huey" selected>Huey</option>
<option value="dewey">Dewey</option>
<option value="louie">Louie</option>
</select>

<!-- and this is the radio tree -->
<fieldset>
<div>
<input type="radio" id="huey" name="drone" value="huey" checked />
<label for="huey">Huey</label>
</div>

<div>
<input type="radio" id="dewey" name="drone" value="dewey" />
<label for="dewey">Dewey</label>
</div>

<div>
<input type="radio" id="louie" name="drone" value="louie" />
<label for="louie">Louie</label>
</div>
</fieldset>

These two are essentially the same.

The main difference is that <select> represents the enum itself, while <input type="radio"> represents one potential value within an enum. One radio button doesn’t even work or input anything on its own, we need to bolt together a set of them to represent an entire enum.

Note: Confusion once again stems from bad terminology. You might intuitively think that radio refers to the collection of several radio buttons, because this is what we see in the real world, but it is not the case. It would be much better to call this <input type="radio-button"> instead.

Another difference is that <input> is a void element (it cannot have any children), but <select> is not. That’s why it is much easier to understand how <select> works, and that is why select couldn’t be an <input> subtype.

Note: The concept of a void element doesn’t make much sense either. We clearly need to add children to <input> elements, in fact, it does have a shadow tree for various subtypes. This begs the question, why not simply allow this:

<input type="radio">
<input type="radio-button">
<input type="radio-button">
<input type="radio-button">
</input>

Anyway, the point is, select and radio represent the exact same concept, they differ only in representation, again, something that has nothing to do with semantics whatsoever.

Array

In some cases, we need more than a single value. HTML allows <select> to be multi-select and in that case we can represent the selected values using an array.

Thus, while it would make sense to combine <select> and <input type="radio">, because they expect the same values, it would also make sense to separate single-select and multi-select, because they expect different values:

<select>
<select-multiple>

Null

The last data type we need to talk about is null. It represents the absence of a value when a field is optional.

Enums, arrays, objects and numbers can all be nullable.

Strings are a bit tricky, because the value of an empty text field is the empty string. We can convert this to null for optional text fields if needed, but we can also work with the empty string as well.

Booleans, however, cannot be null by design. This is because a boolean is actually an enum with two possible values and null would be the third. Think of it like this: a checkbox is either empty or selected, it cannot be anything else.

Putting it all together

To make sense of HTML form elements, first we need to reclassify them according to the type of value they expect from the user:

boolean
<input type="checkbox">
number
<input type="number">
<input type="range">
string
<input type="text">
<input type="email">
<input type="password">
<input type="search">
<input type="tel">
<input type="url">
<textarea>
object
<input type="color">
<input type="date">
<input type="datetime-local">
<input type="time">
<input type="month">
<input type="week">
<input type="file">
enum
<input type="radio">
<select>

Next, we merge elements/types that shouldn’t be separate and separate that shouldn’t be merged.

We’ll also choose better names as well:

Toggle(); //<input type="checkbox">

NumberField(); //<input type="number">
NumberPicker(); //<input type="range">

TextField(); //<input type="text"> <input type="email"> <input type="search"> <input type="tel"> <input type="url"> <textarea>
SecureField(); //<input type="password">

ColorPicker(); //<input type="color">
DatePicker(); //<input type="date">
TimePicker(); //<input type="time">
DateTimePicker(); //<input type="datetime-local">
FilePicker(); //<input type="file">

Picker(); //<select> <input type="radio">
PickerMultiple(); //<select multiple>

Note: Since there is no day input, week is buggy and doesn’t make sense, just like the month input, I did not implement them yet. But the point is, they would still easily fit this new model, along with any other similar inputs.

Finally, any current behavior that isn’t covered by a separate construct should be achieved by a modifier:

TextField()
.textFieldStyle("email"); //email, search, tel, url
TextField()
.whiteSpace("pre"); //same as the white-space CSS property
Picker()
.pickerStyle("radio"); //radio, dropdown, ...

Final notes

There are many other aspects of HTML forms but there is only so much I can shoehorn into a single article.

This is an ongoing project and I am working on many other aspects as well, like buttons, validation, grouping and output elements and I am hoping that I can explore them in another installment.

But your support means a lot.

If you like my articles please clap and share it with your friends and colleagues.

And if you are interested in a particular topic or just want to point out the flaws in my design then please let me know in a comment. I aim to answer all of your messages.

Thank you for reaching the end of this article, have a great day and stay tuned.

⬅️ Part 16 — How to unfuck CSS flex — The ABC of XYZ

--

--

Bence Meszaros
Bence Meszaros

Written by Bence Meszaros

Lead Software Engineer, Fillun & Decketts

Responses (3)