Floating Label Input
Text field whose label floats up on focus — pure CSS.
components/ui/floating-input.tsx
"use client";
import { useId, type InputHTMLAttributes } from "react";
type FloatingInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "placeholder"> & {
label: string;
hint?: string;
};
export function FloatingInput({ label, hint, className = "", id, ...props }: FloatingInputProps) {
const autoId = useId();
const inputId = id ?? autoId;
return (
<div className={className}>
<div className="relative">
<input
id={inputId}
placeholder=" "
className="peer block w-full rounded-xl border border-zinc-200 bg-white px-4 pb-2.5 pt-6 text-sm text-zinc-900 outline-none transition focus:border-blue-600 focus:ring-4 focus:ring-blue-600/10"
{...props}
/>
<label
htmlFor={inputId}
className="pointer-events-none absolute left-4 top-4 origin-left text-sm text-zinc-500 transition-all duration-200 peer-focus:top-2 peer-focus:text-xs peer-focus:text-blue-600 peer-[:not(:placeholder-shown)]:top-2 peer-[:not(:placeholder-shown)]:text-xs"
>
{label}
</label>
</div>
{hint && <p className="mt-1.5 px-1 text-xs text-zinc-500">{hint}</p>}
</div>
);
}
export default function Demo() {
return (
<form onSubmit={(e) => e.preventDefault()} className="w-full max-w-sm space-y-3 rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm">
<h3 className="mb-2 text-lg font-semibold text-zinc-900">Create your account</h3>
<FloatingInput label="Full name" autoComplete="name" />
<FloatingInput label="Work email" type="email" autoComplete="email" hint="We'll never share your email." />
<button className="w-full rounded-xl bg-zinc-900 py-3 text-sm font-medium text-white transition hover:bg-zinc-800">Continue</button>
</form>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/floating-input.tsxcomponents/ui/floating-input.tsx"use client"; import { useId, type InputHTMLAttributes } from "react"; type FloatingInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "placeholder"> & { label: string; hint?: string; }; export function FloatingInput({ label, hint, className = "", id, ...props }: FloatingInputProps) { const autoId = useId(); const inputId = id ?? autoId; return ( <div className={className}> <div className="relative"> <input id={inputId} placeholder=" " className="peer block w-full rounded-xl border border-zinc-200 bg-white px-4 pb-2.5 pt-6 text-sm text-zinc-900 outline-none transition focus:border-blue-600 focus:ring-4 focus:ring-blue-600/10" {...props} /> <label htmlFor={inputId} className="pointer-events-none absolute left-4 top-4 origin-left text-sm text-zinc-500 transition-all duration-200 peer-focus:top-2 peer-focus:text-xs peer-focus:text-blue-600 peer-[:not(:placeholder-shown)]:top-2 peer-[:not(:placeholder-shown)]:text-xs" > {label} </label> </div> {hint && <p className="mt-1.5 px-1 text-xs text-zinc-500">{hint}</p>} </div> ); } export default function Demo() { return ( <form onSubmit={(e) => e.preventDefault()} className="w-full max-w-sm space-y-3 rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm"> <h3 className="mb-2 text-lg font-semibold text-zinc-900">Create your account</h3> <FloatingInput label="Full name" autoComplete="name" /> <FloatingInput label="Work email" type="email" autoComplete="email" hint="We'll never share your email." /> <button className="w-full rounded-xl bg-zinc-900 py-3 text-sm font-medium text-white transition hover:bg-zinc-800">Continue</button> </form> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.