I read this article, which motivated to build the floating form field using two seemingly forgotten HTML elements:
Expectation
Implementation
The MDN page for the legend already shows it doing the floating label trick, at least at the final frame, in combination with a fieldset element.
<fieldset>
<input type="text" id="my-input" placeholder="my input"/>
<legend>
<label for="my-input">my input</label>
</legend>
</fieldset>
What we want to do is:
- Translate the
legendto align with theinput::placeholder. - When the
input::placeholderis not shown, translate thelegendup.
Consider:
- You can translate the label all the way up, above the input, for this example I align it with the top-border of the input field.
- You can also remove the padding added to the left on the legend, or translate it on the X axis when the placeholder is gone.
If you are going to implement this in an application it is better to use proper class names to avoid extremely specific selectors, here I just style the HTML elements.
First style the fieldset element:
- Declare a
CSSvar named--typography-size, set to14px. - Declare a
CSSvar named--base-ratio, set to4px. - Set
font-familytomonospace. - Set
paddingto0 - Set
bordertonone.
The margin in this case is optional, and set for the demo only.
fieldset {
--typography-size: 14px;
--base-ratio: 4px;
font-family: monospace;
padding: 0;
border: none;
margin: 16px;
}
Second, the input element.
- Set the
font-sizeto the--typography-sizevariable. - The
inputfield does not inherit to the root elementfont-familyby default, so do so. - Declare a
borderof1pxwithsolidstyle line, and color#c3c3c3. - Set
marginto0. - Set
paddingto0. - To create spacing between the top border and actual input text, defined
padding-top, in this case,3times the--base-ratio. - Similarly create space at the bottom, in this case,
2times the--base-ratio. - Last but not least, space the text content from the left setting
padding-leftto3times the--base-ratio.
input {
font-size: var(--typography-size);
font-family: inherit;
border: 1px solid #c3c3c3;
margin: 0;
padding: 0;
padding-top: calc(var(--base-ratio) * 3);
padding-bottom: calc(var(--base-ratio) * 2);
padding-left: calc(var(--base-ratio) * 3);
}
It is very important to realize that for the legend, the input text is actually 13px from the left. That is because the border has 1px width. And in general:
3 * var(--base-ratio) + 1px
You could set the input::placeholder to opacity:0 (in the Codepen above it is done like that) but if you want to gracefully hide it when the input gets focused/unfocused do this:
input::placeholder {
opacity: 1;
transition: opacity 0.75s ease;
}
input:focus::placeholder {
opacity: 0;
transition: opacity 0.75s ease;
}
Third the legend element:
- Match the
font-size, using the--typography-sizevariable. - To align the
legendwith theinput::placeholdertext we need to account for13pxor rather3times the--base-ratioand1pxfor the border of the input field.- We break down this into
padding: 0 var(--base-ratio)which implicitly sets thepadding-leftto4px - The other
9pxare set using themargin-leftproperty to2 * var(--base-ratio) + 1px - The combination of padding and margin is done to create
4pxof white space around the x-axis of the legend.
- We break down this into
-
transformthelegendelement in the y-axis by100% + 3 * var(--base-ratio) + 1px. Because theinputelement haspadding-topset to3 * var(--base-ratio), and the1pxbecause of the top border in theinputelement. - Set the
backgroundto transparent color - Make sure all changes have
transition
legend {
font-size: var(--typography-size);
padding: 0 var(--base-ratio);
margin-left: calc(2 * var(--base-ratio) + 1px);
transform: translateY(calc(100% + 3 * var(--base-ratio) + 1px));
background: transparent;
transition: all 0.75s ease;
}
When the input::placeholder is not being shown, the user is typing, select the legend element and change its color to hotpink, background to white, and transform to 50% in the y-axis, translateY(50%). Adjust these values to your desire.
This is the floating label effect.
Optionally, we can set the padding:0, this sets the padding-left:0 implicitly, and the legend will float to the top left corner of the input field.
input:not(:placeholder-shown) ~ legend {
color: hotpink;
background: white;
transform: translateY(50%);
/* optional */
/* padding: 0; */
}
And finally for the label.
- Set the cursor to what one would normally get on a
placeholder, that istextcursor. - Set the
font-sizeto the--typography--sizevariable. - As a bonus,
- Allow
font-sizetransition - When the
input::placeholderis not shown, select thelabeland decrease thefont-size
- Allow
label {
cursor: text;
font-size: var(--typography-size);
transition: font-size 0.75s ease;
}
input:not(:placeholder-shown) ~ legend > label {
font-size: calc(var(--typography) * 0.9);
}
Hopefully I have shown you that HTML elements can already do a lot, the main part of this implementation lies on translating the legend element from its natural position to align with the input::placeholder, the rest are minor tweaks!
There's some more refactoring that could be done at the
fieldsetlevel, for example, calculate3 * var(--base-ratio)as yet anotherCSSvariable.
Happy hacking!

Top comments (0)