Validation States Input
Original · freesuccess/warning/error validated inputs
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-validation-states.json"use client";
import { useId, useState, type ReactNode } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Status = "idle" | "success" | "warning" | "error";
type Result = { status: Status; message: string };
const RING: Record<Status, string> = {
idle: "border-slate-300 dark:border-slate-700 focus-within:border-indigo-500 dark:focus-within:border-indigo-400 focus-within:ring-2 focus-within:ring-indigo-500/25",
success:
"border-emerald-400 dark:border-emerald-500/70 focus-within:ring-2 focus-within:ring-emerald-500/25",
warning:
"border-amber-400 dark:border-amber-500/70 focus-within:ring-2 focus-within:ring-amber-500/25",
error:
"border-rose-400 dark:border-rose-500/70 focus-within:ring-2 focus-within:ring-rose-500/25",
};
const MSG: Record<Status, string> = {
idle: "text-slate-500 dark:text-slate-400",
success: "text-emerald-600 dark:text-emerald-400",
warning: "text-amber-600 dark:text-amber-400",
error: "text-rose-600 dark:text-rose-400",
};
const ICON_TONE: Record<Status, string> = {
idle: "text-slate-400",
success: "text-emerald-500",
warning: "text-amber-500",
error: "text-rose-500",
};
function StatusIcon({ status }: { status: Status }) {
if (status === "success") {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4" fill="currentColor">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.7-9.3a1 1 0 00-1.4-1.4L9 10.6 7.7 9.3a1 1 0 10-1.4 1.4l2 2a1 1 0 001.4 0l4-4z"
clipRule="evenodd"
/>
</svg>
);
}
if (status === "warning") {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4" fill="currentColor">
<path
fillRule="evenodd"
d="M8.26 2.83a2 2 0 013.48 0l6 10.42A2 2 0 0116 16.25H4a2 2 0 01-1.74-3l6-10.42zM10 7a1 1 0 00-1 1v3a1 1 0 002 0V8a1 1 0 00-1-1zm0 7.4a1.15 1.15 0 100 2.3 1.15 1.15 0 000-2.3z"
clipRule="evenodd"
/>
</svg>
);
}
if (status === "error") {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4" fill="currentColor">
<path
fillRule="evenodd"
d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 3.6a1 1 0 011 1v4.2a1 1 0 01-2 0V6.6a1 1 0 011-1zm0 8.1a1.15 1.15 0 100 2.3 1.15 1.15 0 000-2.3z"
clipRule="evenodd"
/>
</svg>
);
}
return null;
}
function EyeIcon({ off }: { off: boolean }) {
return (
<svg
viewBox="0 0 24 24"
aria-hidden="true"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
>
{off ? (
<>
<path d="M3 3l18 18" />
<path d="M10.6 10.7a3 3 0 004.2 4.2" />
<path d="M9.9 5.2A9.6 9.6 0 0112 5c6.5 0 10 7 10 7a17 17 0 01-3.4 4.1" />
<path d="M6.1 6.3A17 17 0 002 12s3.5 7 10 7a9.8 9.8 0 004.1-.9" />
</>
) : (
<>
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" />
<circle cx="12" cy="12" r="3" />
</>
)}
</svg>
);
}
function validateEmail(v: string): Result {
const t = v.trim();
if (t === "") return { status: "idle", message: "We send your confirmation link here." };
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t))
return { status: "error", message: "That does not look like a valid email address." };
if (/@(gmail|yahoo|hotmail|outlook|icloud|proton)\./i.test(t))
return {
status: "warning",
message: "Personal inbox detected. A work email unlocks team seats.",
};
return { status: "success", message: "Looks good. This address is free to use." };
}
const RESERVED = ["admin", "root", "support", "billing", "salman", "webinnoventix"];
function validateUsername(v: string): Result {
if (v === "") return { status: "idle", message: "3 to 20 characters: letters, numbers, underscores." };
if (v.length < 3) return { status: "error", message: "Handle must be at least 3 characters." };
if (!/^[a-z0-9_]+$/i.test(v))
return { status: "error", message: "Use only letters, numbers, and underscores." };
if (RESERVED.includes(v.toLowerCase()))
return { status: "warning", message: "That handle is reserved. Pick another one." };
return { status: "success", message: "@" + v + " is available." };
}
function scorePassword(v: string): number {
let s = 0;
if (v.length >= 8) s += 1;
if (/[a-z]/.test(v) && /[A-Z]/.test(v)) s += 1;
if (/\d/.test(v)) s += 1;
if (/[^A-Za-z0-9]/.test(v)) s += 1;
return s;
}
function validatePassword(v: string, score: number): Result {
if (v === "")
return { status: "idle", message: "Use 8 or more characters with a mix of types." };
if (v.length < 8) return { status: "error", message: "Too short. Use at least 8 characters." };
if (score <= 2)
return { status: "warning", message: "Weak. Add an uppercase letter, number, or symbol." };
if (score === 3)
return { status: "warning", message: "Getting stronger. Add one more character type." };
return { status: "success", message: "Strong password. You are all set." };
}
function validateConfirm(pw: string, c: string): Result {
if (c === "") return { status: "idle", message: "Re-enter your password to confirm." };
if (c !== pw) return { status: "error", message: "These passwords do not match yet." };
return { status: "success", message: "Passwords match." };
}
function validateWebsite(v: string): Result {
const t = v.trim();
if (t === "")
return { status: "idle", message: "Optional. Helps us tailor your onboarding." };
try {
const url = new URL(/^https?:\/\//i.test(t) ? t : "https://" + t);
if (!url.hostname.includes(".") || url.hostname.startsWith("."))
return { status: "error", message: "Enter a valid URL, for example acme.com." };
return { status: "success", message: "We will take a look at " + url.hostname + "." };
} catch {
return { status: "error", message: "Enter a valid URL, for example acme.com." };
}
}
type FieldProps = {
id: string;
label: string;
type?: string;
value: string;
onChange: (v: string) => void;
placeholder?: string;
autoComplete?: string;
inputMode?: "text" | "email" | "url" | "numeric";
result: Result;
optional?: boolean;
reduce: boolean;
trailing?: ReactNode;
children?: ReactNode;
};
function Field({
id,
label,
type = "text",
value,
onChange,
placeholder,
autoComplete,
inputMode,
result,
optional,
reduce,
trailing,
children,
}: FieldProps) {
const msgId = id + "-msg";
const pop = reduce
? undefined
: { animation: "ivs-pop 260ms cubic-bezier(0.34,1.56,0.64,1)" };
return (
<div>
<div className="mb-1.5 flex items-center justify-between gap-2">
<label
htmlFor={id}
className="text-sm font-medium text-slate-700 dark:text-slate-200"
>
{label}
</label>
{optional ? (
<span className="text-[11px] font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
Optional
</span>
) : null}
</div>
<div
className={
"flex items-center rounded-xl border bg-white/80 backdrop-blur-sm transition-[border-color,box-shadow] dark:bg-slate-900/70 " +
RING[result.status]
}
>
<input
id={id}
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
autoComplete={autoComplete}
inputMode={inputMode}
aria-invalid={result.status === "error"}
aria-describedby={msgId}
className="w-full bg-transparent px-4 py-2.5 text-sm text-slate-900 outline-none placeholder:text-slate-400 dark:text-slate-100 dark:placeholder:text-slate-500"
/>
<div className="flex items-center gap-1 pr-2.5">
{trailing}
{result.status !== "idle" ? (
<span
key={result.status}
style={pop}
className={"ivs-anim inline-flex " + ICON_TONE[result.status]}
>
<StatusIcon status={result.status} />
</span>
) : null}
</div>
</div>
{children}
<p
id={msgId}
aria-live="polite"
className={"mt-1.5 text-xs leading-snug " + MSG[result.status]}
>
{result.message}
</p>
</div>
);
}
function StrengthMeter({ score }: { score: number }) {
const tone = [
"bg-slate-200 dark:bg-slate-800",
"bg-rose-500",
"bg-amber-500",
"bg-sky-500",
"bg-emerald-500",
];
const labels = ["", "Weak", "Fair", "Good", "Strong"];
return (
<div className="mt-2 flex items-center gap-2" aria-hidden="true">
<div className="flex flex-1 gap-1.5">
{[1, 2, 3, 4].map((i) => (
<span
key={i}
className={
"h-1.5 flex-1 rounded-full transition-all duration-300 " +
(i <= score ? tone[score] ?? "" : "bg-slate-200 dark:bg-slate-800")
}
/>
))}
</div>
<span className="w-10 shrink-0 text-right text-[11px] font-medium tabular-nums text-slate-500 dark:text-slate-400">
{labels[score] ?? ""}
</span>
</div>
);
}
const SAMPLES: { status: Status; label: string; value: string; message: string }[] = [
{
status: "success",
label: "Coupon code",
value: "LAUNCH25",
message: "Applied. 25% off your first three months.",
},
{
status: "warning",
label: "Display name",
value: "support",
message: "Close to a reserved word. Consider another.",
},
{
status: "error",
label: "Card number",
value: "4242 4242 0000",
message: "This card number is incomplete.",
},
{
status: "idle",
label: "Referral code",
value: "",
message: "Enter a code if a teammate invited you.",
},
];
function SampleField({
status,
label,
value,
message,
index,
}: {
status: Status;
label: string;
value: string;
message: string;
index: number;
}) {
const id = "ivs-sample-" + index;
return (
<div className="rounded-xl border border-slate-200/70 bg-white/60 p-3 dark:border-slate-800/70 dark:bg-slate-900/40">
<div className="mb-1.5 flex items-center justify-between">
<label
htmlFor={id}
className="text-xs font-medium text-slate-600 dark:text-slate-300"
>
{label}
</label>
<span
className={
"text-[10px] font-semibold uppercase tracking-wide " + MSG[status]
}
>
{status}
</span>
</div>
<div
className={
"flex items-center rounded-lg border bg-white px-3 py-2 dark:bg-slate-900 " +
RING[status]
}
>
<input
id={id}
readOnly
value={value}
placeholder="—"
aria-readonly="true"
className="w-full bg-transparent text-sm text-slate-800 outline-none placeholder:text-slate-400 dark:text-slate-100"
/>
{status !== "idle" ? (
<span className={"ml-2 inline-flex " + ICON_TONE[status]}>
<StatusIcon status={status} />
</span>
) : null}
</div>
<p className={"mt-1.5 text-[11px] leading-snug " + MSG[status]}>{message}</p>
</div>
);
}
export default function InpValidationStates() {
const uid = useId();
const reduce = !!useReducedMotion();
const [email, setEmail] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [confirm, setConfirm] = useState("");
const [website, setWebsite] = useState("");
const [showPw, setShowPw] = useState(false);
const [submitted, setSubmitted] = useState<null | "ok" | "err">(null);
const [shakeKey, setShakeKey] = useState(0);
const emailR = validateEmail(email);
const userR = validateUsername(username);
const pwScore = scorePassword(password);
const pwR = validatePassword(password, pwScore);
const confR = validateConfirm(password, confirm);
const webR = validateWebsite(website);
const required = [emailR, userR, pwR, confR];
const formValid =
required.every((r) => r.status === "success" || r.status === "warning") &&
webR.status !== "error";
const clear = () => {
if (submitted) setSubmitted(null);
};
const enter = reduce
? { initial: false as const }
: {
initial: { opacity: 0, y: -6 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -6 },
};
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-20 text-slate-900 sm:py-28 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes ivs-pop {
0% { transform: scale(0.4); opacity: 0; }
60% { transform: scale(1.14); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes ivs-shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-6px); }
40% { transform: translateX(6px); }
60% { transform: translateX(-4px); }
80% { transform: translateX(4px); }
}
@media (prefers-reduced-motion: reduce) {
.ivs-anim { animation: none !important; }
}
`}</style>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-24 left-1/2 h-72 w-[42rem] -translate-x-1/2 rounded-full bg-gradient-to-r from-indigo-300/30 via-violet-300/20 to-emerald-300/30 blur-3xl dark:from-indigo-600/20 dark:via-violet-600/10 dark:to-emerald-600/20"
/>
<div className="relative mx-auto max-w-5xl">
<header className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-xs font-medium text-slate-600 dark:border-slate-800 dark:bg-slate-900/60 dark:text-slate-300">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
Inputs
</span>
<h2 className="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">
Inputs that tell you where you stand
</h2>
<p className="mx-auto mt-3 max-w-xl text-sm text-slate-600 sm:text-base dark:text-slate-400">
Real-time validation with success, warning, and error states.
Accessible, keyboard-ready, and legible in light or dark.
</p>
</header>
<div className="mt-10 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{SAMPLES.map((s, i) => (
<SampleField
key={s.label}
status={s.status}
label={s.label}
value={s.value}
message={s.message}
index={i}
/>
))}
</div>
<form
noValidate
onSubmit={(e) => {
e.preventDefault();
if (formValid) {
setSubmitted("ok");
} else {
setSubmitted("err");
setShakeKey((k) => k + 1);
}
}}
className="mx-auto mt-10 w-full max-w-xl rounded-2xl border border-slate-200 bg-white/70 p-6 shadow-sm backdrop-blur-md sm:p-8 dark:border-slate-800 dark:bg-slate-900/50"
>
<h3 className="text-lg font-semibold">Create your workspace</h3>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
Start a 14-day trial. No card required.
</p>
<AnimatePresence mode="wait" initial={false}>
{submitted === "ok" ? (
<motion.div
key="ok"
role="status"
{...enter}
className="mt-5 flex items-start gap-2.5 rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-800 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-300"
>
<span className="mt-0.5 text-emerald-500">
<StatusIcon status="success" />
</span>
<span>
Workspace ready. We sent a confirmation link to{" "}
<strong className="font-semibold">{email || "your email"}</strong>.
</span>
</motion.div>
) : null}
{submitted === "err" ? (
<motion.div
key={"err-" + shakeKey}
role="alert"
initial={reduce ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="mt-5"
>
<div
className="ivs-anim flex items-start gap-2.5 rounded-xl border border-rose-300 bg-rose-50 px-4 py-3 text-sm text-rose-800 dark:border-rose-500/40 dark:bg-rose-500/10 dark:text-rose-300"
style={reduce ? undefined : { animation: "ivs-shake 380ms ease-in-out" }}
>
<span className="mt-0.5 text-rose-500">
<StatusIcon status="error" />
</span>
<span>Please fix the highlighted fields before continuing.</span>
</div>
</motion.div>
) : null}
</AnimatePresence>
<div className="mt-6 space-y-5">
<Field
id={uid + "-email"}
label="Work email"
type="email"
inputMode="email"
autoComplete="email"
placeholder="you@company.com"
value={email}
onChange={(v) => {
setEmail(v);
clear();
}}
result={emailR}
reduce={reduce}
/>
<Field
id={uid + "-username"}
label="Choose a handle"
autoComplete="username"
placeholder="acme_team"
value={username}
onChange={(v) => {
setUsername(v);
clear();
}}
result={userR}
reduce={reduce}
/>
<Field
id={uid + "-password"}
label="Create password"
type={showPw ? "text" : "password"}
autoComplete="new-password"
placeholder="At least 8 characters"
value={password}
onChange={(v) => {
setPassword(v);
clear();
}}
result={pwR}
reduce={reduce}
trailing={
<button
type="button"
onClick={() => setShowPw((s) => !s)}
aria-pressed={showPw}
aria-label={showPw ? "Hide password" : "Show password"}
className="inline-flex items-center justify-center rounded-md p-1 text-slate-400 transition-colors hover:text-slate-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:hover:text-slate-200"
>
<EyeIcon off={showPw} />
</button>
}
>
{password ? <StrengthMeter score={pwScore} /> : null}
</Field>
<Field
id={uid + "-confirm"}
label="Confirm password"
type={showPw ? "text" : "password"}
autoComplete="new-password"
placeholder="Re-enter your password"
value={confirm}
onChange={(v) => {
setConfirm(v);
clear();
}}
result={confR}
reduce={reduce}
/>
<Field
id={uid + "-website"}
label="Company website"
type="url"
inputMode="url"
autoComplete="url"
placeholder="acme.com"
value={website}
onChange={(v) => {
setWebsite(v);
clear();
}}
result={webR}
optional
reduce={reduce}
/>
</div>
<button
type="submit"
className="mt-7 inline-flex w-full items-center justify-center rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-indigo-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white active:bg-indigo-700 dark:focus-visible:ring-offset-slate-900"
>
Create workspace
</button>
<p className="mt-3 text-center text-xs text-slate-400 dark:text-slate-500">
By continuing you agree to the Terms and Privacy Policy.
</p>
</form>
</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

Floating Label Input
Originallabel that floats up on focus/fill

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

