While browsing The Ordinary’s website resolute in finding the means to battle futuristic wrinkles, I came upon a floating label input field that stole my heart.
Okay, maybe it didn’t steal my heart per se, but it left quite the impression and inspired a desire to build one just like it!
What particularly stands out is the sleek design that includes the seamless float of the label. When not in focus or active states, the label resembles placeholder text. Yet, when focused or activated, two things happen simultaneously to signal interaction:
- the “placeholder text” floats to the top and reduces in size
- the input’s bottom border thickens
This post will be a guided walkthrough of building just such a dynamic input field with a floating label.
See the Pen Floating Label Input Field by Klea Merkuri (@thehelpfultipper) on CodePen.
1. Form with email input field
Start by creating a simple form that contains an input field with a type of email (to add HTML5 validation).
Include a heading – I’m going for an exact replica of the reference – and a cookies disclaimer.
<div id="subscription">
<h3 id="pre-header">CONNECT</h3>
<div id="sub__promo">
<h3 id="title">Stay in touch.</h3>
<form id="sub__form">
<label for="email">Email Address</label>
<input type="email" id="email" />
</form>
</div>
<p id="sub__foot">By providing your email address, you agree to receive communications from us (this can be changed at any time). Please refer to our <a href="#">Privacy Policy</a> and <a href="#">Terms of Use</a> for more details.</p>
</div>
Adding an icon as a button
Notice that in The Ordinary reference, the button is not looking like your typical button. It’s a chevron icon and nowhere do you see an action label for it, like “Subscribe” or “Join”.
This is a good opportunity to use some accessibility attributes. First, add a button element inside the form following the input field. This is a submit button with two accessibility components:
- an aria-label attribute on
<button>
to label the interactive element as “Subscribe” - a
<span>
with a screen reader only class to make the element visible only to a screen reader
<button type="button" id="sub__btn" aria-label="Subscribe">
<span class="sr-reader">Subscribe</span>
</button>
Note: Button is a type of button, not submit, so we avoid form submission as this is only a demo form. When incorporating this in an application and submitting emails to a server, change the type to submit!
2. Styling the subscription form
Let’s make our simple form look more like our reference.
Begin by setting a width for #subscription and centering it on the page.
#subscription {
width: 400px;
margin: 0 auto;
position: relative;
top: 80px;
}
Since the default of the content within #subscription is left-alignment, we need not worry about it.
Next, resize form text elements like the #pre-header and #title. Also, include the appropriate font styles by importing them – I use Google fonts.
At the top of the stylesheet do:
@import {
url("https://fonts.googleapis.com/css2?family=Josefin+Sans&family=MuseoModerno&display=swap");
}
Then specify font-family: 'MuseoModerno', sans-serif
for #subscription.
Follow by adding the font styles for all text.
#pre-header {
font-size: 0.8125rem;
font-weight: 800;
}
#title {
font-family: 'Josefin Sans', sans-serif;
font-size: 1.3125rem;
}
label, #sub__foot {
color: #757575;
}
label {
font-weight: 100;
}
#sub__foot {
font-size: 12px;
line-height: 16px;
font-weight: 400!important;
}
#sub__foot a {
color: #ff01f5;
text-decoration: none;
}
Note: I’m not using the same fonts as the reference so it doesn’t look exactly the same. Quite frankly, I didn’t want to scour too much for the fonts, but feel free to do so!
Customizing the input and button
Have the input extend the width of the container and give it a height. To get rid of the blue outline when focusing on the input field, set outline as none.
input {
width: 100%;
height: 40px;
outline: none;
font-size: 16px;
padding: 10px 40px 10px 0;
}
The input field is looking rather bulky because the padding is added on top of the width and height to produce a massive unappealing rectangle. But we can easily maintain our dimensions and still have our padding by setting box-sizing for everything!
* {
box-sizing: border-box;
}
Now let’s move the button to the right, and inside, of the input. Mind you, it’s not inside so much as it lies on top using absolute positioning with a right direction set as 0.
#sub__btn {
position: absolute;
right: 0;
border: none;
width: 2.5rem;
height: 2.5rem;
}
And to position the screen reader-only element – the span with the text identifying the button – off the visual area of the browser window do:
.sr-reader {
position: absolute;
left: -10000px;
width: 1px;
height: 1px;
top: auto;
overflow: hidden;
}
No one other than the screen reader (and the source code) will know the span exists!
Moving onto the icon of the button, I’ll use CSS :after to enter a right chevron look-like. The one used on the reference is a custom icon which I’ve no access to so I’ll make do with what I have.
#sub__btn:after {
content:'>';
font-family: 'MuseoModerno', sans-serif;
font-size: 150%;
font-weight: 100;
color: #8C8C8C;
}
For input, remove all borders save for the bottom border.
input {
border-top: none;
border-left: none;
border-right: none;
border-bottom: 1px solid #AAAAAA;
}
Position the button icon more on the right edge of the input by adding the following to #sub__btn:after.
position: relative;
right: -10px;
top: -10px;
Then position the label further down the input so it seems like a placeholder in an unfocused, inactive state.
position: relative;
top: 25px;
3. Defining CSS state effects
I’ll start with the hover effect on the button icon where the cursor changes into a pointer when hovering with the mouse.
#sub__btn:hover {
cursor: pointer;
}
Next, tackle the input’s bottom border expansion in either active or focus states.
input:active,
input:focus {
border-bottom: 5px solid #AAAAAA;
}
For the label transition in either active or focus states, I’ll incorporate JavaScript because we need a way to manipulate the label when the input is active or focused.
I cannot affect the label in CSS when identifying the input hover effects because the label comes before the input. Since there’s no CSS selector for a sibling that comes before the main element, we can’t affect the label by such means.
But I can listen for when a user clicks in and outside the input field and do something with the label in those cases.
const label = document.querySelector('label');
const input = document.querySelector('#email');
const main = document.querySelector('#main');
main.addEventListener('click', function(e) {
if(e.target === input) {
label.classList.add('isActive');
} else {
label.classList.remove('isActive');
}
});
Here I add a class of isActive (which isn’t defined yet but will contain the transition and resizing of the label) when the user clicks inside the input field. Then, I remove the isActive class when clicking anywhere outside of the input field.
The callback function takes the event (which I chose to call “e”) and the conditional checks if the targeted element is the input. Should the if statement be true, the isActive class is added to the label; should it be false (i.e. a user clicks anywhere that’s not the input), the isActive class is removed.
I wrapped #subscription in a parent called #main that is always 100% of the viewport height.
#main {
min-height: 100vh;
}
Specifying the parent with a height was necessary to make the logic work. Without it, the isActive class wouldn’t be removed when clicking beyond the height of #subscription. And if we didn’t have a parent wrapper at all – like at the start – the removal logic would only apply when clicking within the bounds of #subscription.
Add the isActive class properties of decreased font size and top movement to the stylesheet. Remember, this is the class that will be added using the above JavaScript code – it doesn’t apply to any element in the HTML markup until the event fires in our script.
.isActive {
font-size: 70%;
top: 5px;
}
Don’t forget to add transition: all 0.2s ease-in-out
to label as this defines the directions of the transition!
You should have the following:
Applying active class effects based on input value change
Observe the reference and notice how deleting text from the input field reverts the transition of the label so we are back to the initial state where it represents a placeholder.
But when the user starts typing, the transition fires, and the input field clears from the label as it floats to the top.
Currently, our logic doesn’t allow for that behavior. The click event doesn’t capture changes in input value so the isActive class still applies even when the user has deleted all text, yet, is still in the input field.
We can fix that by listening to the input event and adding or removing the isActive class based on input value changes.
input.addEventListener('input', () => {
if(input.value === '') {
label.classList.remove('isActive');
} else {
label.classList.add('isActive');
}
});
You can stop here as we’re done. But if you want to skip the HTML5 email validation and add your own custom one, follow along with the last step below!
4. How to validate the email
The reference has two validation messages going out every time the form is submitted.
- Empty Input: When the input field is empty, there’s a “This field is required” error message.
- Invalid Input: When the entered value is invalid, there’s a “Please enter a valid E-Mail address” error message.
Thereafter, the bottom border of the input turns red when active or focused. But when the input passes validation, you get a success message.
So, to break down what we need to do:
- validate email based on an input value to determine the error/success message
- if error, turn the input’s bottom border red
- make the error message red in color
- place the error message beneath the input and above the footer
Go back to the HTML file
In the HTML file, change the type of input from email to text so we don’t activate HTML5 validation.
Then find the p tag with the id of sub__foot and change it from an id to a class. Place right above it another p tag with a class of sub__foot, <p class="sub__foot"></p>
.
Note: You can assign classes and avoid changing the id into a class. Totally up to you as long as you’re able to identify elements.
That covers all HTML changes.
Update and add to the CSS file
Remember to change any #sub__foot selectors to .sub__foot to account for the change in attribute!
Add some more space between the input and the footer(s) and a slight transition to .sub__foot.
margin-top: 20px;
transition: all 0.2s ease-in-out;
Now let’s create some classes that will apply conditionally based on email validation checks. One is going to add a red border at the bottom of the input when validation fails and another is going to style the success message.
.invalid-border {
border-bottom: 5px solid red;
}
.success {
font-family: 'Josefin Sans', sans-serif;
font-size: 1.3125rem;
}
Validate the email in JavaScript
Now let’s apply those classes we created and set a logic flow.
Set variables pointing to the form’s button and paragraphs with a sub__foot class.
const btn = document.querySelector('#sub__btn');
const foot = document.querySelectorAll('.sub__foot');
If you’re following along, recall that we need to use querySelectorAll() to grab all elements with the specified class. So foot here is a list and accessing elements within – in our case, the two p tags – requires either looping or referencing based on an index.
Next, let’s create a function to validate the email. A google search on how to validate email in JavaScript directs us to use a regex expression. We’ll be checking the input value against this regex to determine validation.
The email is “invalid” when the regex match fails (i.e. returns null) or the input value is an empty string (i.e. there’s nothing entered). Based on the validation check, we’ll update a variable with the appropriate message which this function returns at the end.
const validateEmail = input => {
const regex = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/g;
let mssg = '';
if(input.match(regex) !== null) {
// email is valid
mssg = 'Thank you for staying in touch :-)';
} else if(input === '') {
// input is empty
mssg = 'This field is required';
} else {
// input invalid
mssg = 'Please enter a valid E-Mail address';
}
return mssg;
}
Since hitting enter will submit the form by default, our next step is to prevent submission as this is a demo and the form contains to action to submit to (aka we get an error redirect).
document.querySelector('form').addEventListener('submit', e => {
e.preventDefault();
});
Now let’s listen for a button click to execute the email validation function and display the appropriate message.
btn.addEventListener('click', () => {
let message = validateEmail(input.value);
if(message.includes('Thank you')) {
// success message
document.querySelector('#sub__promo').innerHTML = `<h3 class="success">${message}</h3>`;
foot[0].style.display = 'none';
} else {
// error message
foot[0].innerText = message;
input.classList.add('invalid-border');
foot[0].style.color = 'red';
}
});
Above, the message variable stores the value returned by the email validation function. Then a conditional determines if it’s a success message (the one that starts with “Thank you”) or an error message.
For a success message, I’m taking the contents of #sub__promo, the div containing the title and form, to replace them with a single h3 that has the success class we defined earlier in the CSS stylesheet. I, also, hide any error messages (should there have been prior attempts that resulted in errors).
Meanwhile, for error messages, add the message to the empty p tag we added in the HTML file in red color. Recall foot is a list and that p tag happens to be the first item in that list! This is where you give the input a red border-bottom as well.
The last thing is to correct the behavior of the isActive class. This will happen when we listen to the click event for main. Here, two things need to happen:
- keep isActive when the clicked object is the input field or the clicked object is the button and the input value isn’t empty (otherwise the label will overlap with the input value)
- remove isActive when the clicked object isn’t the input and the input value is empty (if the input value isn’t empty, we avoid overlap of label and value)
So a click on main looks like this now:
main.addEventListener('click', function(e) {
if(e.target === input || e.target === btn && input.value !== '') {
label.classList.add('isActive');
} else if(e.target !== input && input.value === '') {
label.classList.remove('isActive');
}
});
And that’s a wrap, my friends! We just completed a sleek, modern, and interactive submission form. Grab the full source code on GitHub.
More: How To Build An Interactive Menu Component