No doubt you’ve seen the beautiful text field if you’re one of Gmail’s 2 billion active users:

It’s fluid, it’s intuitive, it’s colorful 🎨.
It’s Material Design: the wildly popular UI design system powering YouTube, WhatsApp, and many other apps with billions of users.

Let’s embark on a journey of recreating it from scratch with pure vanilla HTML, CSS, and JavaScript.
1. Start: Create basic input and label
As always we start with the critical HTML foundation, the skeleton:
The text input, a label, and a wrapper for both:
<!-- For text animation -- soon -->
<div class="input-container">
<input
type="text"
id="fname"
name="fname"
value=""
aria-labelledby="label-fname"
/>
<label class="label" for="fname" id="label-fname">
<div class="text">First Name</div>
</label>
</div>

2. Style input and label
I find it pretty satisfying: using CSS to gradually flesh out a stunning UI on the backs of a solid HTML foundation.
Let’s start:
Firs the <input>
and its container:
.input-container {
position: relative; /* parent of .label */
}
input {
height: 48px;
width: 280px;
border: 1px solid #c0c0c0;
border-radius: 4px;
box-sizing: border-box;
padding: 16px;
}
.label {
/* to stack on input */
position: absolute;
top: 0;
bottom: 0;
left: 16px; /* match input padding */
/* center in .input-container */
display: flex;
align-items: center;
}
.label .text {
position: absolute;
width: max-content;
}

3. Remove pointer events
It resembles a text field now, but look what happens when I try focusing:

The label is part of the text field and the cursor should reflect that:
Solution? cursor: text
.label {
...
cursor: text;
/* Prevent blocking <input> focus */
pointer-events: none;
}

4. Style input font
Now it’s time to customize font settings:
If you know Material Design well, you know Roboto is at the center of everything — much to the annoyance of some.
We’ll grab the embed code from Google Fonts:

Embed:

Use:
input,
.label .text {
font-family: 'Roboto';
font-size: 16px;
}

5. Style input on focus
You’ll do this with the :focus
selector:
CSS
input:focus {
outline: none;
border: 2px solid blue;
}
✅

6. Fluidity magic: Style label on input focus
On focus the label does 3 things:
- Shrinks
- Move to top input border
- Match input border color
Of course we can do all these with CSS:
input:focus + .label .text {
/* 1. Shrinks */
font-size: 12px;
/* 2. Move to top input border */
transform: translate(0, -100%);
top: 15%;
padding-left: 4px;
padding-right: 4px;
/* 3. Match input border color */
background-color: white;
color: #0b57d0;
}
All we need to complete the fluidity is CSS transition
:
label .text {
transition: all 0.15s ease-out;
}

7. One more thing
Small issue: The label always goes to the original position after the input loses focus:

:focus
which goes away on focus lost.But this should only happen when there’s no input yet.
CSS can’t fix this alone, we’re going to deploy the entire 3-tiered army of web dev.
HTML: input
value
to zero.
<input
type="text"
id="fname"
name="fname"
value=""
aria-labelledby="label-fname"
/>
CSS: :not
selector to give unfocused input label same position and size when not empty:
input:focus + .label .text,
/* ✅ no input yet */
:not(input[value='']) + .label .text {
/* 1. Shrink */
font-size: 12px;
transform: translate(0, -100%);
/* 2. Move to top */
top: 15%;
padding-left: 4px;
padding-right: 4px;
/* 3. Active color */
background-color: white;
color: #0b57d0;
}
And JavaScript: Sync initial input
value
attribute with user input
const input = document.getElementById('fname');
input.addEventListener('input', () => {
input.setAttribute('value', input.value);
});
const input = document.getElementById('fname');
input.addEventListener('input', () => {
input.setAttribute('value', input.value);
});
✅

That’s it! We’ve successfully created an outlined Material Design text field.
With React or Vue it’ll be pretty easy to abstract everything we’ve done into a reusable component.
Here’s the link to the full demo: CodePen
Every Crazy Thing JavaScript Does
A captivating guide to the subtle caveats and lesser-known parts of JavaScript.
