This approach helps maintain a clean and minimalist form design, especially useful for forms with multiple fields.
The solution I found leverages the CSS :placeholder-shown pseudo-class combined with the adjacent sibling combinator (+).
This method requires no JavaScript, offering a purely CSS-based solution that’s both simple and effective.
Lets assume our form html looks like that
<form>
<div>
<label for="name">Name</label>
<input type="text" id="name" name="name" placeholder="Name">
</div>
<div>
<label for="email">E-mail</label>
<input type="email" id="email" name="email" placeholder="E-mail">
</div>
</form>

Lets hide labels. So form will look like this now, no label is shown no matter what.
<style>
...
form label {
display: none;
}
...
</style>

Lets fix that by adding some magic to our css. Now label will be shown as soon as input placeholder will disappear
<style>
...
form label {
display: none;
}
form div label:has(+input:not(:placeholder-shown)) {
display: block;
}
...
</style>

Let’s break down what form div label:has(+input:not(:placeholder-shown)) { display: block; } means:
form div label: This part of the selector targets label elements that are descendants of a div, which itself is a descendant of a form. It’s a way to specify that we’re focusing on labels within forms that follow this particular nesting structure.
:has(+input:not(:placeholder-shown)): This is where the real magic happens, and it’s composed of several parts:
- :has(…): The :has pseudo-class is used to select an element if it matches the condition inside the parentheses. In this case, the condition is related to a sibling input element.
- +: The adjacent sibling combinator. This targets an element that is immediately preceded by the former element. So, label:has(+input) would look for a label that is immediately followed by an input.
- input:not(:placeholder-shown): This selects an input element that does not show its placeholder. The :not() pseudo-class negates the condition inside it, and :placeholder-shown targets input fields that are displaying placeholder text. Together, they select input fields that have content typed into them or have been interacted with in a way that the placeholder is no longer shown.
- { display: block; }: This CSS declaration block will apply the display: block; style to the labels that meet the criteria defined by the selector. Essentially, it means that if the label has a sibling input element where the placeholder is not shown (indicating the user has started filling out the form), then the label will be displayed as a block-level element.
An animated label doesn’t just appear; it catches the eye, guiding the user’s attention in a more dynamic and interactive manner.
Let’s do that:
<style>
...
form label {
display: block;
opacity: 0;
}
form div label:has(+input:not(:placeholder-shown)) {
-webkit-animation: fadeInFromNone 0.5s ease-out forwards;
-moz-animation: fadeInFromNone 0.5s ease-out forwards;
-o-animation: fadeInFromNone 0.5s ease-out forwards;
animation: fadeInFromNone 0.5s ease-out forwards;
}
@-webkit-keyframes fadeInFromNone {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@-moz-keyframes fadeInFromNone {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@-o-keyframes fadeInFromNone {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeInFromNone {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
...
</style>
