Floating Label Input
The label sits inside the empty field like a placeholder and floats up to the border once you focus or type.
Build a React text input with a floating label using only CSS. Put the <input> and its <label> in a relative wrapper; give the input placeholder=" " (a single space) so :placeholder-shown tells you when it is empty. When empty and unfocused, the label sits vertically centred inside the field at full size. On :focus or when not :placeholder-shown, move the label up with transform: translateY(-145%) scale(0.8), transform-origin left, over 200ms with cubic-bezier(0.16, 1, 0.3, 1), and give it a background matching the field so it cuts through the border. Change the border and label colour to the accent on focus. The label stays a real <label for>. Under prefers-reduced-motion, move the label without the transition. Label text: "Email address".
Source
'use client'
import { useId } from 'react'
import s from './demo.module.css'
export default function FloatingLabel({ labels }: { labels: Record<string, string> }) {
const id = useId()
return (
<div className={s.field}>
<input id={id} className={s.input} type="email" placeholder=" " autoComplete="off" />
<label htmlFor={id} className={s.label}>
{labels.label}
</label>
</div>
)
}
.field { position: relative; width: min(280px, 100%); }
.input {
width: 100%;
height: 54px;
padding: 0 16px;
border: 1px solid var(--stage-rule);
border-radius: 12px;
outline: none;
background: var(--stage-bg);
color: var(--stage-ink);
font: 400 var(--text-base) / 1 var(--font-body);
transition: border-color 200ms var(--ease-out);
}
.input:hover { border-color: var(--stage-muted); }
.input:focus { border-color: var(--stage-accent); }
.input:focus-visible { box-shadow: 0 0 0 3px var(--color-focus); }
.label {
position: absolute;
top: 50%;
left: 12px;
padding: 0 4px;
background: var(--stage-bg);
color: var(--stage-muted);
font: 400 var(--text-base) / 1 var(--font-body);
transform: translateY(-50%);
transform-origin: left;
pointer-events: none;
transition: transform 200ms var(--ease-out), color 200ms var(--ease-out);
}
.input:focus + .label,
.input:not(:placeholder-shown) + .label {
transform: translateY(-145%) scale(0.8);
}
.input:focus + .label { color: var(--stage-accent); }
@media (prefers-reduced-motion: reduce) {
.label, .input { transition: none; }
}