Web InnoventixFreeCode

Floating Label Input

Original · free

label that floats up on focus/fill

byWeb InnoventixReact + Tailwind
inpfloatinglabelinputs
Open

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
inp-floating-label.tsx
"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

motion

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 quote

Similar components

Browse all →