A friendly web development tutorial for capturing user input
HTML form elements let you collect input from your website’s visitors. Mailing lists, contact forms, and blog post comments are common examples for small websites, but in organizations that rely on their website for revenue, forms are sacred and revered.
Forms are the “money pages.” They’re how e-commerce sites sell their products, how SaaS companies collect payment for their service, and how non-profit groups raise money online. Many companies measure the success of their website by the effectiveness of its forms because they answer questions like “how many leads did our website send to our sales team?” and “how many people signed up for our product last week?” This often means that forms are subjected to endless A/B tests and optimizations.
There are two aspects of a functional HTML form: the frontend user interface and the backend server. The former is the appearance of the form (as defined by HTML and CSS), while the latter is the code that processes it (storing data in a database, sending an email, etc). We’ll be focusing entirely on the frontend this chapter, leaving backend form processing for a future tutorial.
Setup
Unfortunately, there’s really no getting around that fact that styling forms is hard. It’s always a good idea to have a mockup representing the exact page you want to build before you start coding it up, but this is particularly true for forms. So, here’s the example we’ll be creating in this chapter:
As you can see, this is a speaker submission form for a fake conference. It hosts a pretty good selection of HTML forms elements: various types of text fields, a group of radio buttons, a dropdown menu, a checkbox, and a submit button.
Create a new Atom project called forms and stick a new HTML file in it called speaker-submission.html. For starters, let’s add the markup for the header. (Hey look! It has some semantic HTML!)
<!DOCTYPE html><htmllang='en'><head><metacharset='UTF-8'/><title>Speaker Submission</title><linkrel='stylesheet'href='styles.css'/></head><body><headerclass='speaker-form-header'><h1>Speaker Submission</h1><p><em>Want to speak at our fake conference? Fill out
this form.</em></p></header></body></html>
Next, create a styles.css file and add the following CSS. It uses a simple flexbox technique to center the header (and form) no matter how wide the browser window is:
Notice that we’re adhering to the mobile-first development approach that we discussed in the Responsive Design chapter. These base CSS rules give us our mobile layout and provide a foundation for the desktop layout, too. We’ll create the media query for a fixed-width desktop layout later in the chapter.
HTML Forms
On to forms! Every HTML form begins with the aptly named <form> element. It accepts a number of attributes, but the most important ones are action and method. Go ahead and add an empty form to our HTML document, right under the <header>:
The action attribute defines the URL that processes the form. It’s where the input collected by the form is sent when the user clicks the Submit button. This is typically a special URL defined by your web server that knows how to process the data. Common backend technologies for processing forms include Node.js, PHP, and Ruby on Rails, but again, we’ll be focusing on the frontend in this chapter.
The method attribute can be either post or get, both of which define how the form is submitted to the backend server. This is largely dependent on how your web server wants to handle the form, but the general rule of thumb is to use post when you’re changing data on the server, reserving get for when you’re only getting data.
By leaving the action attribute blank, we’re telling the form to submit to the same URL. Combined with the get method, this will let us inspect the contents of the form.
Styling Forms
Of course, we’re looking at an empty form right now, but that doesn’t mean we can’t add some styles to it like we would a container <div>. This will turn it into a box that matches our <header> element:
First, we have a container <div> to help with styling. This is pretty common for separating input elements. Second, we have a <label>, which you can think of as another semantic HTML element, like <article> or <figcaption>, but for form labels. A label’s for attribute must match the id attribute of its associated <input/> element.
Third, the <input/> element creates a text field. It’s a little different from other elements we’ve encountered because it can dramatically change appearance depending on its type attribute, but it always creates some kind of interactive user input. We’ll see other values besides text throughout the chapter. Remember that ID selectors are bad—the id attribute here is only for connecting it to a <label> element.
Conceptually, an <input/> element represents a “variable” that gets sent to the backend server. The name attribute defines the name of this variable, and the value is whatever the user entered into the text field. Note that you can pre-populate this value by adding a value attribute to an <input/> element.
Styling Text Input Fields
An <input/> element can be styled like any other HTML element. Let’s add some CSS to styles.css to pretty it up a bit. This makes use of all the concepts from the Hello, CSS, Box Model, CSS Selectors, and Flexbox chapters:
The input[type='text'] part is a new type of CSS selector called an “attribute selector”. It only matches <input/> elements that have a type attribute equal to text. This lets us specifically target text fields opposed to radio buttons, which are defined by the same HTML element (<input type='radio'/>). You can read more about attribute selectors at Mozilla Developer Network.
All of our styles are “namespaced” in a .form-rowdescendant selector. Isolating <input/> and <label> styles like this makes it easier to create different kinds of forms. We’ll see why it’s convenient to avoid global input[type='text'] and label selectors once we get to radio buttons.
Finally, let’s tweak these base styles to create our desktop layout. Add the following media query to the end of our stylesheet.
Check out that awesome use of the flex-direction property to make the <label> appear on top of its <input/> element in the mobile layout, but to the left of it in the desktop layout.
Email Input Fields
The <input/> element’s type attribute also lets you do basic input validation. For example, let’s try adding another input element that only accepts email addresses instead of arbitrary text values:
This works exactly like the type='text' input, except it automatically checks that user entered an email address. In Firefox, you can try typing something that’s not an email address, then clicking outside of the field to make it lose focus and validate its input. It should turn red to show the user that it’s an incorrect value. Chrome and Safari don’t attempt to validate until user tries to submit the form, so we’ll see this in action later in this chapter.
This is more than just validation though. By telling browsers that we’re looking for an email address, they can provide a more intuitive user experience. For instance, when a smartphone browser sees this type='email' attribute, it gives the user a special email-specific keyboard with an easily-accessible @ character.
Also notice the new placeholder attribute that lets you display some default text when the <input/> element is empty. This is a nice little UX technique to prompt the user to input their own value.
There’s a bunch of other built-in validation options besides email addresses, which you can read about on MDN’s <input/> reference. Of particular interest are the required, minlength, maxlength, and pattern attributes.
Styling Email Input Fields
We want our email field to match our text field from the previous section, so let’s add another attribute selector to the existing input[type='text'] rule, like so:
/* Change this rule */.form-rowinput[type='text'] {
background-color: #FFFFFF;
/* ... */
}
/* To have another selector */.form-rowinput[type='text'],
.form-rowinput[type='email'] {
background-color: #FFFFFF;
/* ... */
}
Again, we don’t want to use a plain old input type selector here because that would style all of our <input/> elements, including our upcoming radio buttons and checkbox. This is part of what makes styling forms tricky. Understanding the CSS to pluck out exactly the elements you want is a crucial skill.
Let’s not forget about our desktop styles. Update the corresponding input[type='text'] rule in our media query to match the following (note that we’re preparing for the next few sections with the select, and textarea selectors):
@media only screen and (min-width: 700px) {
/* ... */.form-rowinput[type='text'],
.form-rowinput[type='email'], /* Add */.form-rowselect, /* These */.form-rowtextarea { /* Selectors */width: 250px;
height: initial;
}
/* ... */
}
Since we can now have a “right” and a “wrong” input value, we should probably convey that to users. The :invalid and :validpseudo-classes let us style these states independently. For example, maybe we want to render both the border and the text with a custom shade of red when the user entered an unacceptable value. Add the following rule to our stylesheet, outside of the media query:
.form-rowinput[type='text']:invalid,
.form-rowinput[type='email']:invalid {
border: 1px solid #D55C5F;
color: #D55C5F;
box-shadow: none; /* Remove default red glow in Firefox */
}
Until we include a submit button, you’ll only be able to see this in Firefox, but you get the idea. There’s a similar pseudo-class called :focus that selects the element the user is currently filling out. This gives you a lot of control over the appearance of your forms.
Radio Buttons
Changing the type property of the <input/> element to radio transforms it into a radio button. Radio buttons are a little more complex to work with than text fields because they always operate in groups, allowing the user to choose one out of many predefined options.
This means that we not only need a label for each <input/> element, but also a way to group radio buttons and label the entire group. This is what the <fieldset> and <legend> elements are for. Every radio button group you create should:
Be wrapped in a <fieldset>, which is labeled with a <legend>.
Associate a <label> element with each radio button.
Use the same name attribute for each radio button in the group.
Use different value attributes for each radio button.
Our radio button example has all of these components. Add the following to our <form> element underneath the email field:
<fieldsetclass='legacy-form-row'><legend>Type of Talk</legend><inputid='talk-type-1'name='talk-type'type='radio'value='main-stage' /><labelfor='talk-type-1'class='radio-label'>Main Stage</label><inputid='talk-type-2'name='talk-type'type='radio'value='workshop'checked /><labelfor='talk-type-2'class='radio-label'>Workshop</label></fieldset>
Unlike text fields, the user can’t enter custom values into a radio button, which is why each one of them needs an explicit value attribute. This is the value that will get sent to the server when the user submits the form. It’s also very important that each radio button has the same name attribute, otherwise the form wouldn’t know they were part of the same group.
We also introduced a new attribute called checked. This is a “boolean attribute”, meaning that it never takes a value—it either exists or doesn’t exist on an <input/> element. If it does exist on either a radio button or a checkbox element, that element will be selected/checked by default.
Styling Radio Buttons
We have a few things working against us with when it comes to styling radio buttons. First, there’s simply more elements to worry about. Second, the <fieldset> and <legend> elements have rather ugly default styles, and there’s not a whole lot of consistency in these defaults across browsers. Third, at the time of this writing, <fieldset> doesn’t support flexbox.
But don’t fret! This is a good example of floats being a useful fallback for legacy/troublesome elements. You’ll notice that we didn’t wrap the radio buttons in our existing .form-row class, opting instead for a new .legacy-form-row class. This is because it’s going to be completely separate from our other elements, using floats instead of flexbox.
Start with the mobile and tablet styles by adding the following rules outside of our media query. We want to get rid of the default <fieldset> and <legend> styles, then float the radio buttons and labels so they appear in one line underneath the <legend>:
For the desktop layout, we need to make the <legend> line up with the <label> elements in the previous section (hence the width: 120px line), and we need to float everything to the left so they appear on the same line. Update our media query to include the following:
As far as layouts go, this is a pretty good cross-browser solution. However, customizing the appearance of the actual button is another story. It’s possible by taking advantage of the checked attribute, but it’s a little bit complicated. We’ll leave you to Google “custom radio button CSS” and explore that rabbit hole on your own.
Select Elements (Dropdown Menus)
Dropdown menus offer an alternative to radio buttons, as they let the user select one out of many options. The <select> element represents the dropdown menu, and it contains a bunch of <option> elements that represent each item.
Just like our radio button <input/> elements, we have name and value attributes that get passed to the backend server. But, instead of being defined on a single element, they’re spread across the <select> and <option> elements.
Styling Select Elements
And, also just like our radio buttons, <select> elements are notoriously hard to style. However, there’s a reason for this. Dropdowns are a complex piece of interactivity, and their behavior changes significantly across devices. For instance, on an iPhone, clicking a <select> element brings up a native scrolling UI component that makes it much easier to navigate the menu.
It’s usually a good idea to let the browser/device determine the best way to preset a <select> element, so we’ll be keeping our CSS pretty simple. Unfortunately, even the simplest things are surprisingly hard. For instance, try changing the font size of our <select> element:
.form-rowselect {
width: 100%;
padding: 5px;
font-size: 14px; /* This won't work in Chrome or Safari */
}
This will work in Firefox, but not in Chrome or Safari! To sort of fix this, we can use a vendor-specific prefix for the appearance property:
.form-rowselect {
width: 100%;
padding: 5px;
font-size: 14px; /* This won't work in Chrome or Safari */-webkit-appearance: none; /* This will make it work */
}
The -webkit prefix will only apply to Chrome and Safari (which are powered by the WebKit rendering engine), while Firefox will remain unaffected. This is effectively a hack, and even MDN says not to use this CSS property.
Style difficulties like this are a serious consideration when building a form. If you need custom styles, you may be better off using radio buttons or JavaScript UI widgets. Bootstrap Dropdowns and jQuery Selectmenu’s are common JavaScript solutions for customizing select menus. In any case, at least you now understand the problem. You can read more about <select> issues here.
Textareas
The <textarea> element creates a multi-line text field designed to collect large amounts of text from the user. They’re suitable for things like biographies, essays, and comments. Go ahead and add a <textarea> to our form, along with a little piece of instructional text:
<divclass='form-row'><labelfor='abstract'>Abstract</label><textareaid='abstract'name='abstract'></textarea><divclass='instructions'>Describe your talk in 500 words or less</div></div>
Note that this isn’t self-closing like the <input/> element, so you always need a closing </textarea> tag. If you want to add any default text, it needs to go inside the tags opposed to a value attribute.
Styling Textareas
Fortunately, styling textareas is pretty straightforward. Add the following to our styles.css file (before the media query):
By default, many browsers let the user resize <textarea> elements to whatever dimensions they want. We disabled this here with the resize property.
We also need a little tweak in our desktop layout. The .instructions<div> needs to be underneath the <textarea>, so let’s nudge it left by the width of the <label> column. Add the following rule to the end of our media query:
@media only screen and (min-width: 700px) {
/* ... */.form-row.instructions {
margin-left: 120px;
}
}
Checkboxes
Checkboxes are sort of like radio buttons, but instead of selecting only one option, they let the user pick as many as they want. This simplifies things, since the browser doesn’t need to know which checkboxes are part of the same group. In other words, we don’t need a <fieldset> wrapper or shared name attributes. Add the following to the end of our form:
<divclass='form-row'><labelclass='checkbox-label'for='available'><inputid='available'name='available'type='checkbox'value='is-available'/><span>I’m actually available the date of the talk</span></label></div>
The way we used <label> here was a little different than previous sections. Instead of being a separate element, the <label> wraps its corresponding <input/> element. This is perfectly legal, and it’ll make it easier to match our desired layout. It’s still a best practice to use the for attribute.
Styling Checkboxes
For the mobile layout, all we need to do is override the margin-bottom that we put on the rest the <label> elements. Add the following to styles.css, outside of the media query:
.form-row.checkbox-label {
margin-bottom: 0;
}
And inside the media query, we have to take that 120-pixel label column into account:
@media only screen and (min-width: 700px) {
/* ... */.form-row.checkbox-label {
margin-left: 120px;
width: auto;
}
}
By wrapping both the checkbox and the label text, we’re able to use a width: auto to make the entire form field be on a single line (remember that the auto width makes the box match the size of its contents).
Submit Buttons
Finally, let’s finish off our form with a submit button. The <button> element represents a button that will submit its containing <form>:
Clicking the button tells the browser to validate all of the <input/> elements in the form and submit it to the action URL if there aren’t any validation problems. So, you should now be able to type in something that’s not an email address into our email field, click the <button>, and see an error message.
This also gives us a chance to see how the user’s input gets sent to the server. First, enter some values into all the <input/> fields, making sure the email address validates correctly. Then, click the button and inspect the resulting URL in your browser. You should see something like this:
Everything after the ? represents the variables in our form. Each <input/>’s name attribute is followed by an equal sign, then its value, and each variable is separated by an & character. If we had a backend server, it’d be pretty easy for it to pull out all this information, query a database (or whatever), and let us know whether the form submission was successful or not.
Styling Buttons
We had some experience styling buttons in the pseudo-classes section of the CSS Selectors chapter. Back then, we were applying these styles to an <a> element, but we can use the same techniques on a <button>.
Clean up that ugly default <button> styling by adding the following to our stylesheet:
As with our checkbox, we need to take that 120px label column into account, so include one more rule inside our media query:
@media only screen and (min-width: 700px) {
/* ... */.form-rowbutton {
margin-left: 120px;
}
}
Summary
In this chapter, we introduced the most common HTML form elements. We now have all these tools for collecting input from our website visitors:
<input type='text'/>
<input type='email'/>
<input type='radio'/>
<select> and <option>
<textarea>
<input type='checkbox'/>
<button>
You should be pretty comfortable with the HTML and CSS required to build beautiful forms, but actually making these forms functional requires some skills we don’t have yet. That stuff is out of scope for this tutorial, but it might help to have some context. Generally speaking, there are two ways to process forms:
Use the action attribute to send the form data to a backend URL, which then redirects to a success or error page. We got a little glimpse of this in the previous section, and it doesn’t require any JavaScript.
Use AJAX queries to submit the form without leaving the page. Success or error messages are displayed on the same page by manipulating the HTML with JavaScript.
Depending on how your organization is structured, form processing may not be part of your job role as a frontend web developer. If that’s the case, you’ll need to coordinate closely with a backend developer on your team to make sure the <form> submits the correct name-value pairs. Otherwise, it’ll be up to you to make sure the frontend and backend of your forms fit neatly together.
Next, we have our final chapter in HTML & CSS Is Hard. We’ll round out our frontend skills with a thorough discussion of web fonts and practical typographic principles that every web developer should know about.