However, this isn’t necessarily true in the case of the modern web, so let’s learn how to mark up forms using only HTML and CSS.
Form-ing the basic structure
Start off with the
Nothing fancy here. Just covering the basic structure.
<form> ... </form>
action attribute, where the value is the URL you’ll be sending the form data to. The
method should be
POST depending on what you’re trying to achieve (don’t send sensitive data with
Additionally, there’s also the lesser-used
enctype attribute which defines the encoding type of the data being sent. Also, the
target attribute, although not necessarily an attribute unique to forms, can be used to show the output in a new tab.
<form method="POST" action="/subscribe" enctype="application/x-www-form-urlencoded" target="_blank"> ... </form>
Forms are made up of inputs, which expect data values.
<form> <input type="text"><!-- text input --> <input type="text" value="Prefilled value"> </form>
Including Labels for Better Usability & Accessibility
Every input needs a label.
A label is a text descriptor that describes what an input is for. There are three ways to declare a label, but one of them is superior to the other two. Let’s dive into these now.
Adjacent labels require the most code because we need to explicitly declare which input the label describes. To most, this is counterintuitive because we can instead wrap inputs inside labels to achieve the same effect with less code.
That being said, the adjacent method may be necessary in extenuating circumstances, so here’s what that would look like:
<label for="firstName">First name</label> <input id="firstName">
As you can see from the example above, the
for attribute of the
<label> must match the
id attribute of the input, and what this does is explain to input devices which text descriptor belongs to which input. The input device will then relay this to users (screen readers, for example, will dictate it via speech).
While semantic HTML is better, ARIA (Accessible Rich Internet Applications) labels can compensate in their absence. In this case, here’s what a label might look like in the absence of an actual HTML
<input aria-label="First name">
Unfortunately, the downside of this approach is the lack of a visual label. However, this might be fine with some markups (for example, a single-input form with a heading and placeholder):
<h1>Subscribe</h1> <form> <input aria-label="Email address" placeholder="email@example.com"> </form>
(I’ll explain what placeholders are for in a moment.)
Wrapping inputs within labels is the cleanest approach. Also, thanks to CSS’s
:focus-within, we can even style labels while their child inputs receive focus, but we’ll discuss that later.
<label> First name<input> </label>
Placeholders vs labels
A brief comparison:
- Labels state what the input expects
- Placeholders show examples of said expectations
Placeholders aren’t designed to be the alternative to labels, although as we saw in the ARIA example above, they can add back some of the context that’s lost in the absence of visual labels.
Ideally, though, we should use both:
<label> First name<input placeholder="Bruce"> </label>
Choosing Input Types
Placeholders only apply to text-based inputs, but there are actually a whole range of different input types, which include:
<input type="button"> <input type="checkbox"> <input type="color"> <input type="date"> <input type="datetime-local"> <input type="email"> <input type="file"> <input type="hidden"> <!-- explained later --> <input type="image"> <input type="month"> <input type="number"> <input type="password"> <input type="radio"> <input type="range"> <input type="reset"> <input type="search"> <input type="submit"> <!-- submits a form --> <input type="tel"> <input type="text"> <!-- the default --> <input type="time"> <input type="url"> <input type="week">
Semantic input types are useful during form validation, especially when relying on native validation, which we’ll take a look at shortly. First, let’s learn how to style these inputs.
Arguably the most infuriating aspect of coding forms is overriding the browser’s default styling. Thankfully, today,
appearance: none; has 96.06% browser support according to caniuse.com.
After resetting the web browser’s default styling with the following CSS code, we can then style inputs however we want, and this even includes both the radio and checkbox input types:
input -webkit-appearance: none; -moz-appearance: none; appearance: none; ...
However, some of these inputs might come with quirks that are difficult or even impossible to overcome (depending on the web browser). For this reason, many developers tend to fall back to the default
type="text" attribute=value if they find these quirks undesirable (for example, the “caret” on
However, there is a silver lining …
With 82.3% web browser support according to caniuse.com, the new
inputmode attribute specifies which keyboard layout will be revealed on handheld devices irrespective of the input
type being used.
Better than nothing, right?
<input type="text" inputmode="none"> <!-- no keyboard 👀 --> <input type="text" inputmode="text"> <!-- default keyboard --> <input type="text" inputmode="decimal"> <input type="text" inputmode="numeric"> <input type="text" inputmode="tel"> <input type="text" inputmode="search"> <input type="text" inputmode="email"> <input type="text" inputmode="url">
Validating User Input
inputmode achieves nothing in this regard.
inputmode="email" won’t validate an email address, whereas
input type="email" will. That’s the difference.
Putting this aside, let’s discuss what does trigger validation:
<input required> <!-- value is required --> <form required> <!-- all values are required --> <!-- alternative input types --> <input type="email"> <!-- blank or a valid email address --> <input type="email" required> <!-- must be a valid address --> <!-- text-based inputs --> <input minlength="8"> <!-- blank or min 8 characters --> <input maxlength="32"> <!-- blank or max 32 characters --> <input maxlength="32" required> <!-- max 32 characters --> <!-- numeric-based inputs --> <input type="date" min="yyyy-mm-dd"> <!-- min date --> <input type="number" max="66" required> <!-- max number -->
Creating custom rules
RegExp object (but, without wrapping slashes or quotes). Here’s an example that enforces lowercase characters (a–z) and minlength/maxlength in one rule:
More info here.
Note: front-end validation (native-HTML or otherwise) should never, ever be used as a substitute for server-side validation!
Styling valid/invalid states
Just for extra clarity, this is how we’d style validity:
input:valid border-left: 1.5rem solid lime; input:invalid border-left: 1.5rem solid crimson; form:invalid /* this also works, technically! */
Houston, we have a problem!
Inputs attempt to validate their values (or lack thereof) immediately, so the following code (which only shows the valid/invalid states while the input holds a value) might be better:
A Guide to HTML & CSS Forms (No Hacks!)