Recreate the Material Design text field with HTML, CSS, and JavaScript

Last updated on May 18, 2024
Recreate the Material Design text field with HTML, CSS, and JavaScript

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:

  1. Shrinks
  2. Move to top input border
  3. 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:

Because it depends on CSS :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

See also