Using form elements in web apps for input fields provides a better UX and DX. Let me convince you.

It's about time web developers embrace the <form> again when dealing with user input. It provides a better developer experience in fewer lines of code.

When I started using the computers back in 2005, I was very fascinated by how pressing Enter/Return would submit my input. This would happen almost everywhere on the OS level: passwords, folder paths in the Windows File Explorer, etc. Then I started using the web and there was similar UX on individual websites.

But since then, something somewhere has changed. I encounter a lot of websites and mobile apps where pressing Enter/Return just does not submit user input. On my computer, I need to reach my mouse and click a button to submit the user input. On my phone, I need to hide the keyboard and tap a button to submit. This feels backward.

This post is aimed to motivate (but I am also begging) web engineers to return to creating experiences where the keyboard submits user input. I have not written mobile apps in about half a decade, so I am going to leave someone more familiar with that domain to write something similar.

Prefer reacting to the submit event on forms

You can submit forms by pressing Enter/Return or a similar combination (Ctrl+Enter/Return or Cmd+Enter/Return, if you are currently focused on a textarea). You do not need to reach out to press a button. Heck, you do not even need a button to exist.

But, what if:

My button is somewhere far away from where my form is

There are valid use cases for user interfaces where a submit button is in some other part of the DOM tree. A typical example of this would be a modal's footer:

<Modal>
<ModalHeader>...</ModalHeader>
<ModalBody>
<label>What's your name?</label>
<input type="text" name="name" />
</ModalBody>
<ModalFooter>
<button type="button" onClick={submitHandler}>
Submit
</button>
</ModalFooter>
</Modal>

Since the button is not a part of the tree, developers end up adding a click listener to the button, which submits the user input.

There is a better way to handle this. Buttons have a form attribute that takes the ID of the form that you want to submit. This button can be anywhere in your DOM tree. It does not have to be a descendant of the form that you are trying to submit. Then, you can replace the click listener on the button with a listener for the submit event on the form.

The only catch is that now you have to give IDs to your forms. And since two elements that exist in the DOM at the same time should not have the same ID, this gets a little tricky. The simplest fix is to generate an ID and use it on the form and button. If you are using React 18+, there is a built-in useId hook that you can use. For older versions of React, you can use the react-use-uuid package.

import { useId } from 'react';

function BetterComponent() {
const formId = useId();

return (
<Modal>
<ModalHeader>...</ModalHeader>
<ModalBody>
<form id={formId} onSubmit={submitHandler}>
<label>What's your name?</label>
<input type="text" name="name" />
</form>
</ModalBody>
<ModalFooter>
<button type="submit" form={formId}>
Submit
</button>
</ModalFooter>
</Modal>
);
}

For class components and for other libraries/frameworks like Vue, Svelte, etc. you can generate a UUID, store it for the component's lifecycle, and use that.

import { v4 as uuidv4 } from 'uuid';

class BetterClassComponent extends React.Component {
formId = uuidv4();

render() {
return (
<Modal>
<ModalHeader>...</ModalHeader>
<ModalBody>
<form id={this.formId} onSubmit={submitHandler}>
<label>What's your name?</label>
<input type="text" name="name" />
</form>
</ModalBody>
<ModalFooter>
<button type="submit" form={this.formId}>
Submit
</button>
</ModalFooter>
</Modal>
);
}
}

It is preferable to use human-readable IDs that are suffixed with a unique identifier for easier debugging (#edit-user-9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d vs #9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d), but this isn't a hard requirement. If you are feeling adventurous, as an exercise, write a function or a hook that returns human-readable IDs only in development environments.

Cool, what if:

There isn't a submit button in my UI design

The neat part about using form elements is that it is not a hard requirement to have a submit button. Sure, a form conceptually has a submit button. But there are UIs that you and I have seen that do not have a button in them. You are presented with an input field and as soon as you press the Enter/Return key, something happens.

Instead of adding a keyup or a keydown event listener (side question: how would you even decide if you should react to key-up or if you should react to key-down instead?) that checks for whether or not the user pressed the Enter/Return key before doing something, you can wrap your input element inside a form and listen to the submit event.

function Before() {
function keyDownHandler(event) {
if (event.key === 'Enter') {
event.preventDefault();

doSomething();
}
}

return <input onKeyDown={keyDownHandler} />;
}

function After() {
function submitHandler(event) {
event.preventDefault();

doSomething();
}

return (
<form onSubmit={submitHandler}>
<input />
</form>
);
}

Typically, you should avoid reinventing functionality that a browser already provides. There are chances that you would miss edge cases that the browser would have handled.

It is nice that frameworks like Remix and SvelteKit are now actively pushing developers towards embracing browser technologies like form by enhancing the DX around them. I am looking forward to a future where interacting with submitting input fields on the web will be as seamless as it used to be.

In the meantime, remember that every time you reduce friction in your UX using the things mentioned here, you are making it easier for your users to get things done. Happy building!