Floating Label Input
Original · freelabel that floats up on focus/fill
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/inp-floating-label.json"use client";
import { useId, useState, type FormEvent, type ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type Variant = "outline" | "filled";
const cx = (...parts: Array<string | false | null | undefined>): string =>
parts.filter(Boolean).join(" ");
/* ------------------------------- icons ------------------------------- */
function IconUser() {
return (
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M20 21a8 8 0 0 0-16 0" />
<circle cx="12" cy="7" r="4" />
</svg>
);
}
function IconMail() {
return (
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<rect x="2.5" y="4.5" width="19" height="15" rx="2.5" />
<path d="m3 6 9 6 9-6" />
</svg>
);
}
function IconLock() {
return (
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<rect x="4.5" y="10.5" width="15" height="10" rx="2.5" />
<path d="M8 10.5V7.5a4 4 0 0 1 8 0v3" />
</svg>
);
}
function IconBuilding() {
return (
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M4 21V6a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v15" />
<path d="M14 9h4a2 2 0 0 1 2 2v10" />
<path d="M3 21h18M8 8h2M8 12h2M8 16h2" />
</svg>
);
}
function IconEye() {
return (
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7Z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
}
function IconEyeOff() {
return (
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M10.6 6.2A9.9 9.9 0 0 1 12 5c6.4 0 10 7 10 7a17 17 0 0 1-3 3.7M6.3 6.3A17 17 0 0 0 2 12s3.6 7 10 7a9.9 9.9 0 0 0 4.2-.9" />
<path d="M9.9 9.9a3 3 0 0 0 4.2 4.2M3 3l18 18" />
</svg>
);
}
function IconCheck({ size = 16 }: { size?: number }) {
return (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="m4 12.5 5 5 11-11" />
</svg>
);
}
function IconArrow() {
return (
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="transition-transform duration-200 group-hover:translate-x-0.5">
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
);
}
/* --------------------------- floating field --------------------------- */
interface FloatingFieldProps {
id: string;
label: string;
value: string;
onChange: (value: string) => void;
onBlur?: () => void;
type?: string;
variant?: Variant;
autoComplete?: string;
inputMode?: "text" | "email" | "numeric" | "tel" | "search";
multiline?: boolean;
leadingIcon?: ReactNode;
trailingSlot?: ReactNode;
hint?: string;
error?: string;
required?: boolean;
}
function FloatingField({
id,
label,
value,
onChange,
onBlur,
type = "text",
variant = "outline",
autoComplete,
inputMode,
multiline = false,
leadingIcon,
trailingSlot,
hint,
error,
required = false,
}: FloatingFieldProps) {
const reduce = useReducedMotion();
const [focused, setFocused] = useState(false);
const msgId = useId();
const floated = focused || value.trim().length > 0;
const invalid = Boolean(error);
const message = error ?? hint;
const describedBy = message ? msgId : undefined;
const restY = multiline ? 26 : 16;
const upY = variant === "outline" ? -9 : 7;
const shape =
variant === "outline"
? "rounded-xl border bg-white dark:bg-slate-900"
: "rounded-t-xl border-b-2 bg-slate-100 dark:bg-slate-800/70";
const stateRing = invalid
? "border-rose-400 focus-within:border-rose-500 focus-within:ring-4 focus-within:ring-rose-500/15 dark:border-rose-500/70 dark:focus-within:border-rose-400"
: "border-slate-300 focus-within:border-indigo-500 focus-within:ring-4 focus-within:ring-indigo-500/15 dark:border-slate-700 dark:focus-within:border-indigo-400";
const controlClass = cx(
"peer w-full bg-transparent text-[15px] text-slate-900 caret-indigo-500 outline-none dark:text-slate-100 dark:caret-indigo-400",
multiline ? "min-h-[7rem] resize-none pb-3 pt-7" : "h-14",
leadingIcon ? "pl-11" : "pl-3.5",
trailingSlot ? "pr-11" : "pr-3.5",
);
return (
<div>
<div
className={cx(
"relative w-full transition-[border-color,box-shadow,background-color] duration-200",
shape,
stateRing,
)}
>
{leadingIcon ? (
<span
className={cx(
"pointer-events-none absolute left-3.5 text-slate-400 transition-colors duration-200 peer-focus:text-indigo-500 dark:text-slate-500 dark:peer-focus:text-indigo-400",
multiline ? "top-4" : "top-1/2 -translate-y-1/2",
)}
>
{leadingIcon}
</span>
) : null}
{multiline ? (
<textarea
id={id}
value={value}
onChange={(e) => onChange(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => {
setFocused(false);
onBlur?.();
}}
rows={3}
aria-invalid={invalid || undefined}
aria-describedby={describedBy}
required={required}
className={controlClass}
/>
) : (
<input
id={id}
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => {
setFocused(false);
onBlur?.();
}}
aria-invalid={invalid || undefined}
aria-describedby={describedBy}
autoComplete={autoComplete}
inputMode={inputMode}
required={required}
className={controlClass}
/>
)}
<motion.label
htmlFor={id}
initial={false}
animate={{ y: floated ? upY : restY, scale: floated ? 0.78 : 1 }}
transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 460, damping: 30 }}
style={{
position: "absolute",
top: 0,
left: leadingIcon ? 44 : 14,
transformOrigin: "left top",
}}
className={cx(
"pointer-events-none select-none whitespace-nowrap text-base font-medium transition-colors duration-200",
invalid
? "text-rose-600 dark:text-rose-400"
: focused
? "text-indigo-600 dark:text-indigo-400"
: "text-slate-500 dark:text-slate-400",
floated && variant === "outline" ? "rounded bg-white px-1 dark:bg-slate-900" : "",
)}
>
{label}
{required ? <span aria-hidden="true"> *</span> : null}
</motion.label>
{trailingSlot ? (
<span
className={cx(
"absolute right-2",
multiline ? "top-3" : "top-1/2 -translate-y-1/2",
)}
>
{trailingSlot}
</span>
) : null}
</div>
{message ? (
<p
id={msgId}
role={invalid ? "alert" : undefined}
className={cx(
"mt-1.5 pl-1 text-xs",
invalid ? "text-rose-600 dark:text-rose-400" : "text-slate-500 dark:text-slate-400",
)}
>
{message}
</p>
) : null}
</div>
);
}
/* ------------------------------ component ------------------------------ */
export default function InpFloatingLabel() {
const reduce = useReducedMotion();
const uid = useId();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [company, setCompany] = useState("");
const [password, setPassword] = useState("");
const [message, setMessage] = useState("");
const [showPw, setShowPw] = useState(false);
const [nameTouched, setNameTouched] = useState(false);
const [emailTouched, setEmailTouched] = useState(false);
const [pwTouched, setPwTouched] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [done, setDone] = useState(false);
const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const strength = password.length === 0 ? 0 : password.length < 8 ? 1 : password.length < 12 ? 2 : 3;
const strengthLabel = strength <= 1 ? "Weak" : strength === 2 ? "Good" : "Strong";
const strengthBar = strength <= 1 ? "bg-rose-500" : strength === 2 ? "bg-amber-500" : "bg-emerald-500";
const nameError =
(nameTouched || submitted) && name.trim().length === 0 ? "Please enter your name." : undefined;
const emailError =
(emailTouched || submitted) && !emailOk
? email.length > 0
? "That doesn't look like a valid email."
: "Email is required."
: undefined;
const pwError =
(pwTouched || submitted) && password.length < 8 ? "Use at least 8 characters." : undefined;
const formValid = name.trim().length > 0 && emailOk && password.length >= 8;
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSubmitted(true);
if (formValid) setDone(true);
};
const reset = () => {
setName("");
setEmail("");
setCompany("");
setPassword("");
setMessage("");
setShowPw(false);
setNameTouched(false);
setEmailTouched(false);
setPwTouched(false);
setSubmitted(false);
setDone(false);
};
const keyframes = `
@keyframes flchk_pop {
0% { transform: scale(0.3); opacity: 0; }
60% { transform: scale(1.18); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes flblob_drift {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(0, -18px, 0) scale(1.07); }
}
.flchk_anim { animation: flchk_pop 0.32s cubic-bezier(0.34, 1.56, 0.64, 1) both; }
.flblob_anim { animation: flblob_drift 11s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.flchk_anim, .flblob_anim { animation: none !important; }
}
`;
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-20 dark:bg-slate-950 sm:py-24">
<style>{keyframes}</style>
{/* atmosphere */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="flblob_anim absolute -left-24 top-4 h-72 w-72 rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-500/15" />
<div
className="flblob_anim absolute -right-16 bottom-0 h-80 w-80 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-600/15"
style={{ animationDelay: "-4s" }}
/>
</div>
<div className="relative mx-auto w-full max-w-xl">
<header className="mb-8 text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-indigo-600 backdrop-blur dark:border-slate-800 dark:bg-slate-900/70 dark:text-indigo-400">
Onboarding
</span>
<h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
Create your workspace
</h2>
<p className="mt-2.5 text-[15px] leading-relaxed text-slate-600 dark:text-slate-400">
Set up your account in under a minute. Labels lift into place as you type.
</p>
</header>
<motion.div
initial={reduce ? false : { opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
>
{done ? (
<div className="rounded-3xl border border-emerald-200 bg-white p-8 text-center shadow-xl shadow-slate-900/5 dark:border-emerald-500/25 dark:bg-slate-900">
<div className="mx-auto grid h-14 w-14 place-items-center rounded-full bg-emerald-100 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-400">
<span className="flchk_anim inline-flex">
<IconCheck size={26} />
</span>
</div>
<h3 className="mt-4 text-xl font-bold text-slate-900 dark:text-white">
Workspace created
</h3>
<p className="mx-auto mt-2 max-w-sm text-[15px] text-slate-600 dark:text-slate-400">
Welcome aboard, {name.trim().split(" ")[0] || "there"}. We sent a verification link to{" "}
<span className="font-medium text-slate-900 dark:text-slate-200">{email}</span>.
</p>
<button
type="button"
onClick={reset}
className="mt-6 inline-flex items-center justify-center rounded-xl border border-slate-300 bg-white px-5 py-2.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-indigo-500/30 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800"
>
Create another
</button>
</div>
) : (
<form
noValidate
onSubmit={handleSubmit}
aria-labelledby={`${uid}-title`}
className="rounded-3xl border border-slate-200 bg-white/90 p-6 shadow-xl shadow-slate-900/5 backdrop-blur dark:border-slate-800 dark:bg-slate-900/80 sm:p-8"
>
<h3 id={`${uid}-title`} className="sr-only">
Account details
</h3>
<div className="grid gap-5">
<div className="grid gap-5 sm:grid-cols-2">
<FloatingField
id={`${uid}-name`}
label="Full name"
value={name}
onChange={setName}
onBlur={() => setNameTouched(true)}
autoComplete="name"
leadingIcon={<IconUser />}
required
error={nameError}
/>
<FloatingField
id={`${uid}-company`}
label="Company"
variant="filled"
value={company}
onChange={setCompany}
autoComplete="organization"
leadingIcon={<IconBuilding />}
hint="Optional"
/>
</div>
<FloatingField
id={`${uid}-email`}
label="Work email"
type="email"
inputMode="email"
value={email}
onChange={setEmail}
onBlur={() => setEmailTouched(true)}
autoComplete="email"
leadingIcon={<IconMail />}
required
error={emailError}
hint={!emailError ? "We'll send a verification link here." : undefined}
trailingSlot={
emailOk ? (
<span className="flchk_anim grid h-8 w-8 place-items-center text-emerald-500 dark:text-emerald-400">
<IconCheck />
</span>
) : undefined
}
/>
<div>
<FloatingField
id={`${uid}-password`}
label="Password"
type={showPw ? "text" : "password"}
value={password}
onChange={setPassword}
onBlur={() => setPwTouched(true)}
autoComplete="new-password"
leadingIcon={<IconLock />}
required
error={pwError}
trailingSlot={
<button
type="button"
onClick={() => setShowPw((v) => !v)}
aria-pressed={showPw}
aria-label={showPw ? "Hide password" : "Show password"}
className="grid h-8 w-8 place-items-center rounded-lg text-slate-500 transition hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/50 dark:text-slate-400 dark:hover:text-slate-100"
>
{showPw ? <IconEyeOff /> : <IconEye />}
</button>
}
/>
{password.length > 0 && !pwError ? (
<div className="mt-2.5 flex items-center gap-2.5">
<div className="flex flex-1 gap-1.5" aria-hidden="true">
{[0, 1, 2].map((i) => (
<span
key={i}
className={cx(
"h-1.5 flex-1 rounded-full transition-colors duration-300",
i < strength ? strengthBar : "bg-slate-200 dark:bg-slate-700",
)}
/>
))}
</div>
<span
className={cx(
"text-xs font-medium",
strength <= 1
? "text-rose-600 dark:text-rose-400"
: strength === 2
? "text-amber-600 dark:text-amber-400"
: "text-emerald-600 dark:text-emerald-400",
)}
>
{strengthLabel}
</span>
</div>
) : null}
</div>
<FloatingField
id={`${uid}-brief`}
label="What are you building?"
variant="filled"
multiline
value={message}
onChange={setMessage}
hint="Optional — a sentence is plenty."
/>
<button
type="submit"
className="group mt-1 inline-flex w-full items-center justify-center gap-2 rounded-xl bg-indigo-600 px-5 py-3.5 text-sm font-semibold text-white shadow-lg shadow-indigo-600/25 transition hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-indigo-500/40 active:scale-[0.99] dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
Create workspace
<IconArrow />
</button>
<p className="text-center text-xs text-slate-500 dark:text-slate-400">
Already have an account?{" "}
<button
type="button"
className="rounded font-semibold text-indigo-600 underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/50 dark:text-indigo-400"
>
Sign in
</button>
</p>
</div>
</form>
)}
</motion.div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Basic Set Input
Originaldefault, hover, focus, disabled, error text inputs

Filled Input
Originalfilled material-style inputs

Underline Input
Originalminimal underline inputs with animated bar

Search Input
Originalsearch box with icon and clear

Search Suggest Input
Originalsearch with a live suggestions dropdown

Password Toggle Input
Originalpassword field with show/hide toggle

Textarea Autosize Input
Originaltextarea that grows with content

Addon Group Input
Originalinput group with leading/trailing addons

Currency Input
Originalcurrency input with formatting

Tags Input
Originaltag input, add and remove chips

OTP Input
Original6-box one-time-code input with paste

Phone Input
Originalphone input with country prefix select

