16 CSS Floating Label Inputs08 / 16
CSS Floating Label for Select Dropdowns
The hardest field to float: <select> has no placeholder, so :placeholder-shown never fires. This demo uses a hidden empty first option plus :has() and :valid to float the label only once a real choice is made — with a custom chevron, matching text-input styling, and full keyboard behaviour intact. Variation: the floated label becomes a solid accent PILL CHIP that lands on the top border — a badge, not a shrunken caption.
Published
The code
<section class="flb-08" aria-label="Floating label select demo">
<form class="flb-08__form" novalidate>
<h3 class="flb-08__title">Book a table</h3>
<div class="flb-08__field">
<select class="flb-08__select" id="flb-08-party" name="party" required>
<option value="" selected></option>
<option value="2">2 guests</option>
<option value="4">4 guests</option>
<option value="6">6 guests</option>
<option value="8">8+ guests</option>
</select>
<label class="flb-08__label" for="flb-08-party">Party size</label>
<span class="flb-08__chev" aria-hidden="true"></span>
</div>
<div class="flb-08__field">
<select class="flb-08__select" id="flb-08-time" name="time" required>
<option value="" selected></option>
<option value="18">6:00 PM</option>
<option value="19">7:00 PM</option>
<option value="20">8:00 PM</option>
<option value="21">9:00 PM</option>
</select>
<label class="flb-08__label" for="flb-08-time">Preferred time</label>
<span class="flb-08__chev" aria-hidden="true"></span>
</div>
<button class="flb-08__btn" type="button">Find a table</button>
</form>
</section><section class="flb-08" aria-label="Floating label select demo">
<form class="flb-08__form" novalidate>
<h3 class="flb-08__title">Book a table</h3>
<div class="flb-08__field">
<select class="flb-08__select" id="flb-08-party" name="party" required>
<option value="" selected></option>
<option value="2">2 guests</option>
<option value="4">4 guests</option>
<option value="6">6 guests</option>
<option value="8">8+ guests</option>
</select>
<label class="flb-08__label" for="flb-08-party">Party size</label>
<span class="flb-08__chev" aria-hidden="true"></span>
</div>
<div class="flb-08__field">
<select class="flb-08__select" id="flb-08-time" name="time" required>
<option value="" selected></option>
<option value="18">6:00 PM</option>
<option value="19">7:00 PM</option>
<option value="20">8:00 PM</option>
<option value="21">9:00 PM</option>
</select>
<label class="flb-08__label" for="flb-08-time">Preferred time</label>
<span class="flb-08__chev" aria-hidden="true"></span>
</div>
<button class="flb-08__btn" type="button">Find a table</button>
</form>
</section>.flb-08,
.flb-08 *,
.flb-08 *::before,
.flb-08 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.flb-08 {
--accent: oklch(0.58 0.16 30);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f6f2ee;
padding: 40px 20px;
}
.flb-08__form {
width: min(380px,100%);
background: #fff;
border-radius: 20px;
padding: 32px 30px;
box-shadow: 0 22px 55px -30px rgba(60,30,10,.35);
display: flex;
flex-direction: column;
gap: 20px;
}
.flb-08__title {
font-size: 23px;
font-weight: 800;
color: #26201b;
letter-spacing: -.02em;
}
.flb-08__field {
position: relative;
}
.flb-08__select {
appearance: none;
-webkit-appearance: none;
width: 100%;
height: 56px;
padding: 0 44px 0 16px;
font-size: 16px;
font-family: inherit;
color: #26201b;
background: #fff;
border: 1.5px solid #ddd4cb;
border-radius: 12px;
outline: none;
cursor: pointer;
transition: border-color .2s,box-shadow .2s;
}
.flb-08__select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 4px oklch(0.58 0.16 30 / .14);
}
.flb-08__label {
position: absolute;
left: 16px;
top: 50%;
translate: 0 -50%;
font-size: 16px;
color: #8d8378;
pointer-events: none;
transform-origin: left center;
padding: 3px 10px;
margin-left: -10px;
border-radius: 999px;
transition: transform .22s,color .22s,background-color .22s;
}
.flb-08__select:focus ~ .flb-08__label,
.flb-08__select:valid ~ .flb-08__label {
transform: translateY(-28px) scale(.74);
background: var(--accent);
color: #fff;
}
.flb-08__field:has(.flb-08__select:valid) .flb-08__label {
transform: translateY(-28px) scale(.74);
background: var(--accent);
color: #fff;
}
.flb-08__select:focus ~ .flb-08__label {
color: #fff;
}
.flb-08__chev {
position: absolute;
right: 18px;
top: calc(50% - 7px);
width: 10px;
height: 10px;
border-right: 2.5px solid #8d8378;
border-bottom: 2.5px solid #8d8378;
rotate: 45deg;
pointer-events: none;
transition: rotate .2s,border-color .2s;
}
.flb-08__field:has(.flb-08__select:focus) .flb-08__chev {
rotate: 225deg;
top: calc(50% - 2px);
border-color: var(--accent);
}
.flb-08__btn {
height: 50px;
border: none;
border-radius: 12px;
background: var(--accent);
color: #fff;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: filter .15s;
}
.flb-08__btn:hover {
filter: brightness(1.08);
}
.flb-08__btn:focus-visible,
.flb-08__select:focus-visible {
outline: 3px solid oklch(0.58 0.16 30 / .5);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.flb-08__label,
.flb-08__select,
.flb-08__chev {
transition: none;
}
}.flb-08,
.flb-08 *,
.flb-08 *::before,
.flb-08 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.flb-08 {
--accent: oklch(0.58 0.16 30);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f6f2ee;
padding: 40px 20px;
}
.flb-08__form {
width: min(380px,100%);
background: #fff;
border-radius: 20px;
padding: 32px 30px;
box-shadow: 0 22px 55px -30px rgba(60,30,10,.35);
display: flex;
flex-direction: column;
gap: 20px;
}
.flb-08__title {
font-size: 23px;
font-weight: 800;
color: #26201b;
letter-spacing: -.02em;
}
.flb-08__field {
position: relative;
}
.flb-08__select {
appearance: none;
-webkit-appearance: none;
width: 100%;
height: 56px;
padding: 0 44px 0 16px;
font-size: 16px;
font-family: inherit;
color: #26201b;
background: #fff;
border: 1.5px solid #ddd4cb;
border-radius: 12px;
outline: none;
cursor: pointer;
transition: border-color .2s,box-shadow .2s;
}
.flb-08__select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 4px oklch(0.58 0.16 30 / .14);
}
.flb-08__label {
position: absolute;
left: 16px;
top: 50%;
translate: 0 -50%;
font-size: 16px;
color: #8d8378;
pointer-events: none;
transform-origin: left center;
padding: 3px 10px;
margin-left: -10px;
border-radius: 999px;
transition: transform .22s,color .22s,background-color .22s;
}
.flb-08__select:focus ~ .flb-08__label,
.flb-08__select:valid ~ .flb-08__label {
transform: translateY(-28px) scale(.74);
background: var(--accent);
color: #fff;
}
.flb-08__field:has(.flb-08__select:valid) .flb-08__label {
transform: translateY(-28px) scale(.74);
background: var(--accent);
color: #fff;
}
.flb-08__select:focus ~ .flb-08__label {
color: #fff;
}
.flb-08__chev {
position: absolute;
right: 18px;
top: calc(50% - 7px);
width: 10px;
height: 10px;
border-right: 2.5px solid #8d8378;
border-bottom: 2.5px solid #8d8378;
rotate: 45deg;
pointer-events: none;
transition: rotate .2s,border-color .2s;
}
.flb-08__field:has(.flb-08__select:focus) .flb-08__chev {
rotate: 225deg;
top: calc(50% - 2px);
border-color: var(--accent);
}
.flb-08__btn {
height: 50px;
border: none;
border-radius: 12px;
background: var(--accent);
color: #fff;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: filter .15s;
}
.flb-08__btn:hover {
filter: brightness(1.08);
}
.flb-08__btn:focus-visible,
.flb-08__select:focus-visible {
outline: 3px solid oklch(0.58 0.16 30 / .5);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.flb-08__label,
.flb-08__select,
.flb-08__chev {
transition: none;
}
}/* No JavaScript — a required select with an empty first option is :invalid until a real choice is made; select:valid (plus :has() on the wrapper) floats the label. Native keyboard, type-ahead and mobile pickers stay intact. */
/* No JavaScript — a required select with an empty first option is :invalid until a real choice is made; select:valid (plus :has() on the wrapper) floats the label. Native keyboard, type-ahead and mobile pickers stay intact. */Here's a working CSS Floating Label Input from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Floating Label for Select Dropdowns
Source: https://codefronts.com/components/css-floating-label-inputs/css-floating-label-for-select-dropdowns/
The hardest field to float: <select> has no placeholder, so :placeholder-shown never fires. This demo uses a hidden empty first option plus :has() and :valid to float the label only once a real choice is made — with a custom chevron, matching text-input styling, and full keyboard behaviour intact. Variation: the floated label becomes a solid accent PILL CHIP that lands on the top border — a badge, not a shrunken caption.
## HTML
```html
<section class="flb-08" aria-label="Floating label select demo">
<form class="flb-08__form" novalidate>
<h3 class="flb-08__title">Book a table</h3>
<div class="flb-08__field">
<select class="flb-08__select" id="flb-08-party" name="party" required>
<option value="" selected></option>
<option value="2">2 guests</option>
<option value="4">4 guests</option>
<option value="6">6 guests</option>
<option value="8">8+ guests</option>
</select>
<label class="flb-08__label" for="flb-08-party">Party size</label>
<span class="flb-08__chev" aria-hidden="true"></span>
</div>
<div class="flb-08__field">
<select class="flb-08__select" id="flb-08-time" name="time" required>
<option value="" selected></option>
<option value="18">6:00 PM</option>
<option value="19">7:00 PM</option>
<option value="20">8:00 PM</option>
<option value="21">9:00 PM</option>
</select>
<label class="flb-08__label" for="flb-08-time">Preferred time</label>
<span class="flb-08__chev" aria-hidden="true"></span>
</div>
<button class="flb-08__btn" type="button">Find a table</button>
</form>
</section>
```
## CSS
```css
.flb-08,
.flb-08 *,
.flb-08 *::before,
.flb-08 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.flb-08 {
--accent: oklch(0.58 0.16 30);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f6f2ee;
padding: 40px 20px;
}
.flb-08__form {
width: min(380px,100%);
background: #fff;
border-radius: 20px;
padding: 32px 30px;
box-shadow: 0 22px 55px -30px rgba(60,30,10,.35);
display: flex;
flex-direction: column;
gap: 20px;
}
.flb-08__title {
font-size: 23px;
font-weight: 800;
color: #26201b;
letter-spacing: -.02em;
}
.flb-08__field {
position: relative;
}
.flb-08__select {
appearance: none;
-webkit-appearance: none;
width: 100%;
height: 56px;
padding: 0 44px 0 16px;
font-size: 16px;
font-family: inherit;
color: #26201b;
background: #fff;
border: 1.5px solid #ddd4cb;
border-radius: 12px;
outline: none;
cursor: pointer;
transition: border-color .2s,box-shadow .2s;
}
.flb-08__select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 4px oklch(0.58 0.16 30 / .14);
}
.flb-08__label {
position: absolute;
left: 16px;
top: 50%;
translate: 0 -50%;
font-size: 16px;
color: #8d8378;
pointer-events: none;
transform-origin: left center;
padding: 3px 10px;
margin-left: -10px;
border-radius: 999px;
transition: transform .22s,color .22s,background-color .22s;
}
.flb-08__select:focus ~ .flb-08__label,
.flb-08__select:valid ~ .flb-08__label {
transform: translateY(-28px) scale(.74);
background: var(--accent);
color: #fff;
}
.flb-08__field:has(.flb-08__select:valid) .flb-08__label {
transform: translateY(-28px) scale(.74);
background: var(--accent);
color: #fff;
}
.flb-08__select:focus ~ .flb-08__label {
color: #fff;
}
.flb-08__chev {
position: absolute;
right: 18px;
top: calc(50% - 7px);
width: 10px;
height: 10px;
border-right: 2.5px solid #8d8378;
border-bottom: 2.5px solid #8d8378;
rotate: 45deg;
pointer-events: none;
transition: rotate .2s,border-color .2s;
}
.flb-08__field:has(.flb-08__select:focus) .flb-08__chev {
rotate: 225deg;
top: calc(50% - 2px);
border-color: var(--accent);
}
.flb-08__btn {
height: 50px;
border: none;
border-radius: 12px;
background: var(--accent);
color: #fff;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: filter .15s;
}
.flb-08__btn:hover {
filter: brightness(1.08);
}
.flb-08__btn:focus-visible,
.flb-08__select:focus-visible {
outline: 3px solid oklch(0.58 0.16 30 / .5);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.flb-08__label,
.flb-08__select,
.flb-08__chev {
transition: none;
}
}
```
## JavaScript
```js
/* No JavaScript — a required select with an empty first option is :invalid until a real choice is made; select:valid (plus :has() on the wrapper) floats the label. Native keyboard, type-ahead and mobile pickers stay intact. */
```Here's a working CSS Floating Label Input from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Floating Label for Select Dropdowns
Source: https://codefronts.com/components/css-floating-label-inputs/css-floating-label-for-select-dropdowns/
The hardest field to float: <select> has no placeholder, so :placeholder-shown never fires. This demo uses a hidden empty first option plus :has() and :valid to float the label only once a real choice is made — with a custom chevron, matching text-input styling, and full keyboard behaviour intact. Variation: the floated label becomes a solid accent PILL CHIP that lands on the top border — a badge, not a shrunken caption.
## HTML
```html
<section class="flb-08" aria-label="Floating label select demo">
<form class="flb-08__form" novalidate>
<h3 class="flb-08__title">Book a table</h3>
<div class="flb-08__field">
<select class="flb-08__select" id="flb-08-party" name="party" required>
<option value="" selected></option>
<option value="2">2 guests</option>
<option value="4">4 guests</option>
<option value="6">6 guests</option>
<option value="8">8+ guests</option>
</select>
<label class="flb-08__label" for="flb-08-party">Party size</label>
<span class="flb-08__chev" aria-hidden="true"></span>
</div>
<div class="flb-08__field">
<select class="flb-08__select" id="flb-08-time" name="time" required>
<option value="" selected></option>
<option value="18">6:00 PM</option>
<option value="19">7:00 PM</option>
<option value="20">8:00 PM</option>
<option value="21">9:00 PM</option>
</select>
<label class="flb-08__label" for="flb-08-time">Preferred time</label>
<span class="flb-08__chev" aria-hidden="true"></span>
</div>
<button class="flb-08__btn" type="button">Find a table</button>
</form>
</section>
```
## CSS
```css
.flb-08,
.flb-08 *,
.flb-08 *::before,
.flb-08 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.flb-08 {
--accent: oklch(0.58 0.16 30);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f6f2ee;
padding: 40px 20px;
}
.flb-08__form {
width: min(380px,100%);
background: #fff;
border-radius: 20px;
padding: 32px 30px;
box-shadow: 0 22px 55px -30px rgba(60,30,10,.35);
display: flex;
flex-direction: column;
gap: 20px;
}
.flb-08__title {
font-size: 23px;
font-weight: 800;
color: #26201b;
letter-spacing: -.02em;
}
.flb-08__field {
position: relative;
}
.flb-08__select {
appearance: none;
-webkit-appearance: none;
width: 100%;
height: 56px;
padding: 0 44px 0 16px;
font-size: 16px;
font-family: inherit;
color: #26201b;
background: #fff;
border: 1.5px solid #ddd4cb;
border-radius: 12px;
outline: none;
cursor: pointer;
transition: border-color .2s,box-shadow .2s;
}
.flb-08__select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 4px oklch(0.58 0.16 30 / .14);
}
.flb-08__label {
position: absolute;
left: 16px;
top: 50%;
translate: 0 -50%;
font-size: 16px;
color: #8d8378;
pointer-events: none;
transform-origin: left center;
padding: 3px 10px;
margin-left: -10px;
border-radius: 999px;
transition: transform .22s,color .22s,background-color .22s;
}
.flb-08__select:focus ~ .flb-08__label,
.flb-08__select:valid ~ .flb-08__label {
transform: translateY(-28px) scale(.74);
background: var(--accent);
color: #fff;
}
.flb-08__field:has(.flb-08__select:valid) .flb-08__label {
transform: translateY(-28px) scale(.74);
background: var(--accent);
color: #fff;
}
.flb-08__select:focus ~ .flb-08__label {
color: #fff;
}
.flb-08__chev {
position: absolute;
right: 18px;
top: calc(50% - 7px);
width: 10px;
height: 10px;
border-right: 2.5px solid #8d8378;
border-bottom: 2.5px solid #8d8378;
rotate: 45deg;
pointer-events: none;
transition: rotate .2s,border-color .2s;
}
.flb-08__field:has(.flb-08__select:focus) .flb-08__chev {
rotate: 225deg;
top: calc(50% - 2px);
border-color: var(--accent);
}
.flb-08__btn {
height: 50px;
border: none;
border-radius: 12px;
background: var(--accent);
color: #fff;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: filter .15s;
}
.flb-08__btn:hover {
filter: brightness(1.08);
}
.flb-08__btn:focus-visible,
.flb-08__select:focus-visible {
outline: 3px solid oklch(0.58 0.16 30 / .5);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.flb-08__label,
.flb-08__select,
.flb-08__chev {
transition: none;
}
}
```
## JavaScript
```js
/* No JavaScript — a required select with an empty first option is :invalid until a real choice is made; select:valid (plus :has() on the wrapper) floats the label. Native keyboard, type-ahead and mobile pickers stay intact. */
```How this works
<select> can't use the placeholder trick, so this pattern flips the constraint API into a state machine: the first <option> is empty, disabled-selectable (value="") and the select is required. While the empty option is chosen the select is :invalid; the moment the user picks a real option it becomes :valid — and .field:has(select:valid) (with a sibling-selector fallback) floats the label. Focus floats it too, so the label is out of the way while the dropdown is open.
The native arrow is replaced with appearance:none plus a CSS-drawn chevron that rotates on focus. Because it's still a real <select>, arrow keys, type-ahead, mobile pickers and screen readers all work untouched.
Techniques used: required select + empty value="" option state machine · :valid/:invalid on select · :has() wrapper styling with sibling (~) fallback · appearance:none · CSS-drawn chevron (rotated borders) with rotate transition · pill chip via padding+border-radius+background-color transition landing on the border
Make it yours
- Add options freely — the float logic never needs updating.
- Restyle the chevron (a rotated border square) or swap in an SVG background.
- If the select is genuinely optional, keep
requiredfor the float and skip validation by not using a submit flow, or move to a JS-free 'always floated' label. - Recolour via
--accent.
Gotchas — read before shipping
- The empty first option MUST have
value=""— any other value makes the select permanently :valid and the label floats immediately. - Don't rebuild selects as div-dropdowns just for a floating label; you lose free keyboard, mobile and AT behaviour.
- Safari <15.4 lacks :has() — the included
select:valid ~ labelfallback rule covers it.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 15.4+ | 121+ | 111+ |
Floor set by :has(), oklch() as this demo is written. The core technique itself goes back further — Chrome 105+ (:has), older via sibling fallback.
The :valid trick itself is universal; :has() only adds container-level styling. Both paths ship in this demo.