Filling out forms online has never been exactly fun, and it can be downright painful on a mobile device with its on-screen keyboard and limited viewing space. Fortnuately, modern browsers help make this easier by providing new semantic input types and validation. These features, in conjunction with a solid user interface design approach, make it much more possible for users to provide the information needed, increasing completion rates, while improving the user experience and accuracy.

Label & Input Structure

To avoid unnecessary warnings and janky functionality, it’s important to keep your input tags properly wrapped within a label and associate the label with the input by adding the input’s id attribute to the label’s htmlFor attribute.

<label htmlFor={inputId}>
    <input id={inputId} type="text" />
</label>

Many linters will flag this as an error if you do not associate an input with a label as in the example. To avoid other issues when debugging, it’s a good start to follow the structure above when building your input fields with labels. Below you can see how we can add other elements within the label tag and stylize or even animate microinteractions for the user, guiding them through the form.

Stylized HTML5 Input Fields

HTML5 introduces a number of new input types that give hints to the browser about what type of on-screen keyboard layout to display. Many browsers provide built-in validation for some input types like email and url. On other elements, you can indicate a valid input format by providing a regular expression in the pattern attribute.

<input id="username" type="text" pattern="[A-Z]{3}[0-9]{4}" />

Input Type Description
url For entering a URL. It must start with a valid URI scheme, (for example http://, ftp:// or mailto:).
tel For entering phone numbers. It does not enforce a particular syntax for validation, so if you want to ensure a particular format, you can use pattern.
email For entering email addresses. By default it will only take one, but if the multiple attribute is provided, a comma separated list of email addresses is valid.
password For entering passwords securely by hiding the input's value dynamically.
search A text input field styled in a way that is consistent with the platform's search field.
number For numeric input, can be any rational integer or float value.
color For choosing colors.
range For number input, but unlike the number input type, the value is less important. It is displayed to the user as a slider control.
datetime For entering a date and time value where the time zone is provided as GMT.
datetime-local For entering a date and time value where the time zone provided is the local time zone.
date For entering a date (only) with no time zone provided.
time For entering a time (only) with no time zone provided.
week For entering a date that consists of a week-year number and a week number, but no time zone.
month For entering a date with a year and a month, but no time zone.

Adding the required attribute forces the field to contain a value before the form can be submitted.

<input id="username" type="text" pattern="[A-Z]{3}[0-9]{4}" required />

For numeric input types like number or range, you can specify the minimum and maximum values, as well as how much they should either increase or decrease when adjusted by the slider or spinners.

<input type="number" id="qty" min="0" max="100" step="1" />

The maxlength attribute can be used to specify the maximum length of an input or textbox and is useful when you want to limit the length of information that the user can provide.

<input type="text" id="fileName" maxlength="12" />

Data validation doesn’t just help to keep your data clean, but it also helps improve the user experience. For example, datalists can provide auto-complete in text boxes suggesting options to the user. Warnings can be displayed when the users response doesn’t match the expected format. The javaScript validation APIs can provide custom validation logic and prompt users with easy to understand error messages when they’ve provided invalid data. The table below lists the new javaScript APIs and properties that are available on input elements.

API Use
willValidate Property that returns true or false if the element is a candidate for validation.
validity Property that returns a ValidityState object representing the validity states of the element.
validationMessage Property that returns a string with the reason the object failed the validation test.
checkValidity() Returns true if the element satisfies all of it’s constraints, and false otherwise.
setCustomValidity() Sets a custom validation message and the customError property of the ValidityState object to true.

CSS3 Selectors

The :placeholder-shown selector is relatively new and doesn’t have complete browser support yet. However, this seems like something that could easily work as a progressive enhancement. The selector allows us to detect whether a placeholder is currently visible to the user. This could come in handy if we want to dynamically hide and show the input’s associated label.

Use the :required selector to target an input field if it has the required attribute. The same works for the :optional selector which is the exact opposite of required. By being able to target these selectors, we can have the form tell the user these fields are required with the styles we add to the input thanks to the attribute and selector.

The disabled attribute also has a selector, :disabled. If an input field is marked disabled, we can notify the user. With javascript we can add/remove these attributes and the selector styles will change accordingly.

Other selectors we can use to work with validation and vanilla javascript include :valid, :invalid, :in-range, and :out-of-range. We can tell the user if any input type's value is valid or invalid for submission and style accordingly. Also, in cases with input fields with number type that our value is either in or out of range from our set default values.

Custom Iconography

Trends in HTML form design have seen higher use of icons within input fields, as well as integrating them into responses and validation of the form. By using the proper structure of labels and inputs, we can incorporate other floating elements within the block. We can also add iconography to the :before and :after selectors to display dynamically with javascript. In the following example we’re using custom icons with some of the new input types, attributes and selectors.

fieldset {
    width: 100%;
}

label {
    position: relative;
    display: block;
}

label.required:after {
    position: absolute;
    content: '*';
    color: red;
    font-size: 1.2rem;
    font-weight: 900;
    right: -12px;
}

input {
    position: relative;
    width: 100%;
    background: #FFFFFF;
    border: 1px solid #E9E9E9;
    padding: 10px 10px 10px 40px;
}

label.required input {
    border: 1px solid red;
}

.formIcon {
    position: absolute;
    font-size: 2.5rem;
    color: #C0DEFF;
    margin: 0;
    padding: 8px;
}

<fieldset>
    <label htmlFor="username">
        <img class="formIcon" src="user.svg" alt="User Icon" />
        <input type="text" id="username" placeholder="Username" maxlength="12" required />
    </label>
</fieldset>
<fieldset>
    <label htmlFor="password">
        <img class="formIcon" src="key.svg" alt="Password Icon" />
        <input type="password" id="password" placeholder="********" />
    </label>
</fieldset>

Note that we cannot use pseudoelements :before and :after on input tags due to their nature of not being a container and cannot support children. Targeting the label, we can add a .required class and dynamically add and remove it with javascript through validation. Depending on the design it may also be present from the start as in the example above.

Select & Datalist

The datalist element isn’t an input type, but a list of suggested input values that may be associated with an input form field. It lets the browser suggest autocomplete options as the user types. Unlike select elements where users must scan a predetermined limited list and choose a value, datalists provide hints dynamically as a user inputs a value.

Styling these elements requires a bit of resetting and removing inheritted styles before we can proceed with a custom look.

select {
    // A reset of styles, including removing the default dropdown arrow
    appearance: none;
    // Additional resets for further consistency
    width: 100%;
    background-color: transparent;
    margin: 0;
    padding: 0 1rem 0 0;
    font-family: inherit;
    font-size: inherit;
    cursor: inherit;
    line-height: inherit;
    outline: none;
}

Now we can add some root variables and apply our styles. In this case with a reset above, we will apply the styles to a class called .example but you may merge the styles with the rarely used attribute appearance: none;. Ideally we just need to hide the default browser styles and replace them with our own.

:root {
    --select-border: #2e6da4;
    --select-focus: #2e6da4;
    --select-arrow: var(--select-border);
}

label {
    position: relative;
    display: grid;
    grid-area-templates: "select";
    grid-area: select;
    align-items: center;
}

label:after {
    position: absolute;
    grid-area: select;
    justify-self: end;
    content: '';
    width: 0.8rem;
    height: 0.5rem;
    background-color: var(--select-arrow);
    clip-path: polygon(100% 0%, 0 0%, 50% 100%);
    right: 10px;
}

select.example {
    background: linear-gradient(to top, #adadad, #fff 35%);
    width: 100%;
    min-width: 200px;
    max-width: 100%;
    border: 1px solid var(--select-border);
    border-radius: 0.25rem;
    padding: 0.25rem 0.5rem;
    font-size: 1.25rem;
    cursor: pointer;
    line-height: 1.1rem;
}

As for datalists, you will need to apply some javascript to override the browser controls. This is not something done with CSS alone, unfortunately, but it is in the interest of the user to retain some browser styles since they are overwhelmingly familiar to the user. In this situation, it’s similar to trying to override the default keyboard. It may be a bit too much with little to no user experience benefits.

Checkboxes & Radio Buttons

Similar to selects and datalists, a checkbox or radio input also comes with challenges, yet the same starting points apply. Once we clear the browser styles by applying an appearance of none, we can start to add custom styles to our elements. The actual check or radio marker will be added from the pseudoelement :before with the CSS3 selector :checked that notifies us the checkbox or radio button has been selected.

label {
    position: relative;
    display: flex;
    align-items: center;
    width: 100%;
    margin: 5px auto;
}

label span {
    cursor: pointer;
}

input[type=checkbox] {
    appearance: none;
    background: radial-gradient(ellipse at top, #fff 33%, #adadad);
    width: 1rem;
    height: 1rem;
    border: 1px solid gray;
    border-radius: 1rem;
    color: green;
    margin-right: 0.5rem;
    cursor: pointer;
}

input[type=checkbox]:before {
    content: "✔";
    position: absolute;
    font-size: 1.2rem;
    top: -0.3rem;
    visibility: hidden;
}

input[type="checkbox"]:checked::before {
    // Use visibility instead of display to avoid recalculating layout
    visibility: visible;
}

input[type="checkbox"]:disabled, .disabled span {
    opacity: 0.6;
    cursor: not-allowed;
}

Additional Tools

Several other common form features are provided by new attributes that would have previously required additional code.

There may be cases where you don’t want the browser to automatically fill in data based on users past entries. You may want to use this in fields like one-time passwords, pin numbers or password hints. In this case, you can add the autocomplete='off' to either the form element or an individual element.

On some forms, like a custom search page for example, one would want the focus to immediately jump to a specific input search field so the user can begin filling out the form. While there are javascript tools to do this, they can tend to be annoying and clunky when they move focus after the user already started typing. Instead, we can use the autofocus attribute on an input element to specify that element as the primary form element.

You can see in the examples above how we applied a placeholder attribute which adds content to the input as a background element. It can tell the user what to add in that field or simply suggest to begin typing.

Beyond the constraint validation API, there are a few other new functions and properties that are helpful for input elements.

API Use
.stepUp()
.stepDown()
Functions for numeric and date inputs, allowing the adjustment of values to its next logical step.
.valueAsNumber Property that returns the value as a number, useful for numeric inputs instead of having to do parseInt(element.value, 10) or parseFloat(element.value, 10).
.valueAsDate Property that returns the value as a Date object, useful for date inputs instead of having to manually parse them.

The final form

A good user interface begs for a form to be legible and easy to navigate. Utilizing these newer HTML5 tags and CSS3 selectors allows a frontend developer to design and develop successful forms for any website or web application.