<label class="field">
<span class="label">Label</span>
<input type="text" placeholder="Placeholder" />
</label>
<label class="field filled">
<span class="label">Label</span>
<input type="text" placeholder="Placeholder" />
</label>
Components
Text field
Variants
Sizes
<label class="field small">
<!-- -->
</label>
Supporting text
<label class="field">
<span class="label">Label</span>
<input type="text" placeholder="Placeholder" />
<span class="supporting-text">Supporting text</span>
</label>
Validation
- Add
[required]
to the<input>
element to toggle required styles - The
.error
class toggles the error styles. Make use of the supporting text to give extra feedback on the error.
<label class="field">
<span class="label">Label</span>
<input type="text" placeholder="Placeholder" required/>
</label>
<label class="field error">
<span class="label">Label</span>
<input type="text" placeholder="Placeholder" />
<span class="supporting-text">Supporting text</span>
</label>
Auto-fit
When enabled the Field changes size depending on its content.
<label class="field auto-fit">
<!-- -->
</label>
Auto-fit + labels
Be aware that if your label is long you might experience some animation jank when focusing on the input as a natural result of the input changing size.Input types
Date inputs
Date-related inputs never show as empty, so the label is always visible. There are only hacks with compromises and no neat ways of dealing with that issue. You're free to implement a solution of your own here that works with your project.Numeric vs <input type="number">
<label class="field">
<span class="label">Numeric</span>
<input type="text" inputmode="numeric" pattern="[0-9]*" placeholder="Numeric">
<input type="number" placeholder="Number">
</label>
You most likely don't need <input type="number">
While <input type="number">
may seem logical for numeric data it should only be used when mathematical operations are needed on the input (which is... never). Data like credit card numbers, IDs or social security numbers - are actually text that happen to be numeric rather than mathematical values. Therefore, consider using <input type="text" inputmode="numeric" pattern="[0-9]*">
instead.
You will have a bad time.
This triggers the numeric keyboard on mobile devices while avoiding the jank of number inputs, such as:
- Unexpected value increments from scroll wheels
- Browser-specific validation differences
- Accessibility problems
- Removal of leading zeros
- Allows for some non-numeric mathematical characters
It should probably be called <input type="math">
instead.
The British Government has a great article about how .error input number is and goes in-depth. It's a very interesting read.
File
- Use
aria-label
instead of the<label>
element.
File is a weird one. Should it really be an <input>
element? Well, it's what we've got 😅
<div class="field" aria-label="Label">
<input type="file" placeholder="File" />
</div>
Autosuggest
Leverages the <input>
+ <datalist>
element combo.
<input list="DATALISTID">
<datalist id="DATALISTID">
<div class="field">
<input type="text" list="datalist-id" />
<datalist id="datalist-id">
<option value="option value"></option>
</datalist>
</div>
Think of <datalist>
as a list of suggested values.
<select>
only allows you to choose between its provided values.<input>
lets you input anything you want.<input>
+<datalist>
is a hybrid between the two.
Do I have to use <label>
?
No. But you get some accessibility wins for free with <label>
. It's recommended to label your inputs somehow.
<div class="field auto-fit">
<input type="text" placeholder="Placeholder" />
</div>
Accessibility
- Don't use
<input type="number">
unless your user research tells you to.
Anatomy
label.field
: Container element.label
: Field label element<input>
: Input element.supporting-text
: Supporting text element
API
Field API
Type | Modifiers | Default | Description |
---|---|---|---|
Auto-fit | .auto-fit | - | When enabled, the element changes size depending on its content. |
Children | .label , <input> , <select> , <textarea> , <datalist> , .supporting-text | - | Optional children. |
Sizes | .small | - | The size of the element. |
Validation | .error | - | When applied, error styles are shown. |
Variants | outlined, .filled | outlined | The variant to use. |
Browser compatibility
Installation
@layer components.has-deps {
/*
- Common styling for input, textarea and select
- Form related styles such as: label, supporting text, error handling
*/
:where(.field) {
--_accent-color: var(--primary);
--_bg-color: var(--surface-default);
--_border-color: var(--field-border-color);
--_field-padding-block: 0.75rem;
--_field-padding-inline: var(--size-2);
--_filled-border-color: var(--text-color-1);
--_height: var(--field-size);
--_label-color: var(--text-color-2);
--_supporting-text-color: var(--text-color-2);
display: inline-grid;
position: relative;
/* Input/Select base */
& input,
& textarea,
& select {
background-color: var(--_bg-color);
block-size: var(--_height);
border-radius: var(--field-border-radius);
border: var(--field-border-width) solid var(--_border-color);
color: var(--text-color-1);
font-family: var(--font-sans);
font-size: var(--font-size-1);
grid-column: 1/-1;
grid-row: 1;
inline-size: 100%;
line-height: var(--font-lineheight-1);
min-inline-size: 0;
padding: var(--_field-padding-block) var(--_field-padding-inline);
@media (prefers-reduced-motion: no-preference) {
transition:
border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
padding-block 0.2s var(--ease-3);
}
}
/* Required/Invalid */
&:has(
:not(:placeholder-shown):invalid,
:where(
:placeholder-shown,
option[value=""]:not(:checked),
option:checked:not([value=""])
):required
) {
.label:after {
color: var(--red);
content: "*";
margin: -0.25em auto auto 0.25em;
}
}
/* File */
&:has(input[type="file"]) {
cursor: pointer;
input {
align-self: flex-start;
block-size: var(--_height);
box-shadow: none;
color: var(--text-color-1);
cursor: inherit;
max-inline-size: 100%;
padding: 0;
transition: font-size 0.2s var(--ease-3);
&::-webkit-file-upload-button,
&::file-selector-button {
background-color: var(--surface-tonal);
border: none;
block-size: calc(100% - var(--size-2) * 2);
border-radius: var(--field-border-radius);
cursor: pointer;
margin-inline-end: 1ex;
margin-block-start: var(--size-2);
margin-inline-start: var(--size-2);
}
}
/* Variants */
&.filled {
input {
&::-webkit-file-upload-button,
&::file-selector-button {
background-color: var(--surface-default);
block-size: calc(100% - var(--size-2) * 2);
border-radius: var(--field-border-radius);
cursor: pointer;
margin-block-start: var(--size-2);
}
}
}
/* Sizes */
&.small {
input {
font-size: var(--font-size-sm);
&::-webkit-file-upload-button,
&::file-selector-button {
block-size: calc(100% - var(--size-2));
margin-block-start: var(--size-1);
}
}
}
}
/* Autosuggest */
&:has(input[list]) {
.label {
/* Make sure chevron is visible */
inline-size: calc(100% - var(--size-6));
}
}
/* Experimental Select */
&:has(select button) {
select {
padding: 0;
button {
outline: 0;
padding: var(--_field-padding-block) var(--size-8)
var(--_field-padding-block) var(--_field-padding-inline);
}
}
}
/* Non-experimental Select */
&:has(select):not(:has(button)) {
select {
padding: var(--_field-padding-block) var(--size-8)
var(--_field-padding-block) var(--_field-padding-inline);
}
.label {
/* Make sure chevron is visible */
inline-size: calc(100% - var(--size-6));
}
}
/* Input - color */
&:has(input[type="color"]) {
input {
appearance: none;
background: none;
block-size: var(--_height);
overflow: hidden;
padding: 0;
&::-webkit-color-swatch {
border: none;
}
&::-webkit-color-swatch-wrapper {
padding: 0;
}
}
.label {
border: 1px solid var(--field-border-color);
inline-size: fit-content;
margin-inline-start: var(--size-2);
}
}
/* Textarea */
&:has(textarea) {
.label {
align-self: start;
margin-block-start: var(--_field-padding-block);
}
}
/*
* Variant: Outlined
*/
&:not(.filled) {
/* Element states */
&:hover {
&:not(.error) {
:where(input, textarea, select) {
--_border-color: var(--text-color-1);
}
}
}
}
&:not(.filled):focus-within {
& input,
& textarea,
& select {
border-color: var(--_accent-color);
outline-offset: -2px;
outline: 2px solid var(--_accent-color);
}
}
/* Label */
.label {
align-self: center;
background-color: var(--_bg-color);
border-radius: var(--field-border-radius);
color: var(--_label-color);
border-radius: var(--radius-1);
display: inline-flex;
font-size: var(--font-size-md);
grid-column: 1/-1;
grid-row: 1;
inline-size: calc(100% - (var(--field-border-width) * 2));
margin-inline-start: var(--field-border-width);
padding-inline: var(--_field-padding-inline);
pointer-events: none;
z-index: 1;
@media (prefers-reduced-motion: no-preference) {
transition:
border-color 0.2s var(--ease-3),
font-size 0.2s var(--ease-3),
inline-size 0.05s var(--ease-3),
margin 0.2s var(--ease-3),
padding-inline 0.2s var(--ease-3);
}
}
/*
* Label transitions
* Triggered by:
* - focus
* - filled form fields (except color inputs)
* - non-empty select options
*/
&:focus-within,
&:has(:where(input:not([type="color"]), textarea):not(:placeholder-shown)),
&:has(option[value=""]:not(:checked)),
&:has(option:checked:not([value=""])) {
.label {
border-color: transparent;
color: var(--_accent-color);
font-size: 0.75rem;
inline-size: max-content;
letter-spacing: 0.15px;
line-height: 1.15;
margin-block-start: -2.7rem;
margin-inline-start: var(--_field-padding-inline);
padding-inline: 0.125rem;
}
/* Neutral label color reset */
&:not(:focus-within):not(.error) {
.label {
color: var(--text-color-2);
}
}
&:has(textarea) {
.label {
margin-block-start: -0.35rem;
}
&.small {
.label {
align-self: start;
margin-block-start: var(--_field-padding-block);
}
}
}
}
/* Supporting text */
.supporting-text {
color: var(--_supporting-text-color);
font-size: var(--font-size-xs);
grid-row: 3;
line-height: 1.5;
margin-inline-start: var(--field-border-width);
padding-inline: var(--_field-padding-inline);
z-index: 1;
}
/* Auto-fit */
&.auto-fit {
inline-size: auto;
:where(& input, & textarea) {
field-sizing: content;
}
}
/* Validation */
&.error {
--_accent-color: var(--color-9);
--_border-color: var(--color-9);
--_filled-border-color: var(--color-9);
--_label-color: var(--color-9);
--_supporting-text-color: var(--color-9);
}
/*
* Variant: Filled
*/
&.filled {
--_bg-color: var(--surface-tonal);
*:focus-visible {
outline: 0;
}
/* Base style */
& input,
& textarea,
& select {
border-block-end-color: var(--_filled-border-color);
border-block-start-color: transparent;
border-inline-color: transparent;
border-radius: 0;
}
& input[type="color"] {
border-inline: none;
}
/* Bottom line */
&:before {
background-color: var(--_filled-border-color);
block-size: calc(var(--field-border-width) + 1px);
content: "";
inline-size: 100%;
margin-block-end: calc(-1 * (var(--field-border-width) * 2));
transform: scaleX(0);
translate: 0 calc(-1 * (var(--field-border-width) * 2));
z-index: 1;
@media (prefers-reduced-motion: no-preference) {
transition:
transform 0.3s var(--ease-3),
translate 0.2s var(--ease-3);
}
}
/* Label */
.label {
background-color: var(--_bg-color);
}
&:not(:has([disabled], :has(input[type="color"]))) {
/* Hover */
&:hover {
--_bg-color: light-dark(
oklch(from var(--surface-tonal) calc(l * 0.93) c h),
oklch(from var(--surface-tonal) calc(l * 1.1) c h)
);
}
}
/*
* Label transitions
* Triggered by:
* - focus
* - filled form fields (except color inputs)
* - non-empty select options
*/
&:has(.label) {
&:focus-within,
&:has(
:where(input:not(:where([type="color"])), textarea):not(
:placeholder-shown
)
),
&:has(option[value=""]:not(:checked)),
&:has(option:checked:not([value=""])) {
:where(input, textarea) {
padding-block: calc(var(--_field-padding-block) * 1.7)
calc(var(--_field-padding-block) * 0.3);
}
select > button,
select:not(:has(button)) {
padding-block: calc(var(--_field-padding-block) * 1.7)
calc(var(--_field-padding-block) * 0.3);
}
.label {
margin-block-start: calc(-1 * var(--size-5));
margin-inline-start: calc(var(--_field-padding-inline) / 2);
padding-inline: calc(var(--_field-padding-inline) / 2);
}
&:has(textarea) {
.label {
margin-block-start: var(--size-1);
}
}
}
}
/* Element states */
&:hover {
&:before {
transform: scaleX(1);
}
}
&:focus-within {
& input,
& textarea,
& select {
border-block-end-color: var(--_accent-color);
}
&:before {
background-color: var(--_accent-color);
transform: scaleX(1) translateX(0px);
}
}
}
/* Disabled */
&:where(:has([disabled])) {
&:before {
display: none;
}
:where(input, textarea, select) {
cursor: not-allowed;
opacity: 0.7;
* {
pointer-events: none;
}
}
}
/* Read-only */
&:where(:has([readonly])) {
&:before {
display: none;
}
:where(input, textarea, select) {
cursor: not-allowed;
* {
pointer-events: none;
}
}
}
/* Sizes */
&.small {
--_field-padding-block: var(--size-2);
--_height: var(--field-size-small);
&:has(input[type="color"]) {
.label {
line-height: 1.5;
}
}
&:has(textarea) {
.label {
align-self: center;
margin-block-start: unset;
}
}
/*
* Label transitions
* Triggered by:
* - focus
* - filled form fields (except color inputs)
* - non-empty select options
*/
&:focus-within,
&:has(
:where(input:not([type="color"]), textarea):not(:placeholder-shown)
),
&:has(option[value=""]:not(:checked)),
&:has(option:checked:not([value=""])) {
.label {
margin-block-start: -2.2rem;
margin-inline-start: var(--size-1);
padding-inline: var(--size-1);
}
&:not(.filled):has(textarea) {
.label {
margin-block-start: -0.35rem;
}
}
}
}
}
}
@layer components.has-deps {
:where(
.field:has(
:where(
input[type="date"],
input[type="datetime-local"],
input[type="email"],
input[type="month"],
input[type="number"],
input[type="password"],
input[type="search"],
input[type="tel"],
input[type="text"],
input[type="time"],
input[type="url"],
input[type="week"]
)
)
) {
/* Sizes */
&.small {
input {
padding-inline: var(--size-2);
}
}
}
/* Autosuggest */
:where(.field:has(input[list])) {
/* Hide native arrow */
input::-webkit-calendar-picker-indicator {
opacity: 0;
position: absolute;
cursor: pointer;
pointer-events: none;
}
}
:where(
.field:has(input[list]:placeholder-shown),
.field:has(input[list]):where(:focus-within, :hover)
) {
/* Arrow */
&:after {
block-size: 0;
border-block-start: 5px solid;
border-inline: 5px solid transparent;
content: "";
display: inline-block;
flex-shrink: 0;
inline-size: 0;
inset: 50% var(--size-3) auto auto;
pointer-events: none;
position: absolute;
translate: 0 -50%;
}
}
}