Web InnoventixFreeCode

Basic Set Input

Original · free

default, hover, focus, disabled, error text inputs

byWeb InnoventixReact + Tailwind
inpbasicsetinputs
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-basic-set.json
inp-basic-set.tsx
"use client";

import {
  type FormEvent,
  type ReactNode,
  type Ref,
  useEffect,
  useId,
  useRef,
  useState,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Status = "default" | "error" | "success";
type StateId = "default" | "hover" | "focus" | "disabled" | "error";
type FieldKey = "name" | "email" | "password";

const cn = (...parts: Array<string | false | null | undefined>) =>
  parts.filter(Boolean).join(" ");

/* ---------------- inline SVG icons ---------------- */

function MailIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.75}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <rect x="3" y="5" width="18" height="14" rx="2" />
      <path d="m4 7.5 8 5.5 8-5.5" />
    </svg>
  );
}

function UserIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.75}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <circle cx="12" cy="8" r="4" />
      <path d="M4 20c0-3.6 3.6-6 8-6s8 2.4 8 6" />
    </svg>
  );
}

function LockIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.75}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <rect x="4" y="11" width="16" height="9" rx="2" />
      <path d="M8 11V8a4 4 0 0 1 8 0v3" />
    </svg>
  );
}

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2.25}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="m5 13 4 4L19 7" />
    </svg>
  );
}

function AlertIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <circle cx="12" cy="12" r="9" />
      <path d="M12 8v4.5" />
      <path d="M12 16h.01" />
    </svg>
  );
}

function EyeIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.75}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z" />
      <circle cx="12" cy="12" r="3" />
    </svg>
  );
}

function EyeOffIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.75}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="M10.7 6.2A9.9 9.9 0 0 1 12 6c6 0 9.5 6 9.5 6a17 17 0 0 1-3.2 3.9" />
      <path d="M6.3 7.8A16.7 16.7 0 0 0 2.5 12S6 18 12 18a9.7 9.7 0 0 0 3.5-.7" />
      <path d="M9.9 9.9a3 3 0 0 0 4.2 4.2" />
      <path d="m3 3 18 18" />
    </svg>
  );
}

function SuccessTick() {
  return (
    <span className="pointer-events-none grid h-8 w-8 place-items-center">
      <CheckIcon className="inpbasic-pop h-4 w-4 text-emerald-500" />
    </span>
  );
}

/* ---------------- reusable field ---------------- */

interface TextFieldProps {
  id: string;
  label: string;
  value: string;
  onChange: (value: string) => void;
  type?: string;
  placeholder?: string;
  autoComplete?: string;
  inputMode?: "text" | "email" | "search" | "numeric";
  helper?: string;
  error?: string;
  status?: Status;
  disabled?: boolean;
  hoverPreview?: boolean;
  optional?: boolean;
  leadingIcon?: ReactNode;
  trailing?: ReactNode;
  onBlur?: () => void;
  inputRef?: Ref<HTMLInputElement>;
}

function TextField(props: TextFieldProps) {
  const {
    id,
    label,
    value,
    onChange,
    type = "text",
    placeholder,
    autoComplete,
    inputMode,
    helper,
    error,
    disabled = false,
    hoverPreview = false,
    optional = false,
    leadingIcon,
    trailing,
    onBlur,
    inputRef,
  } = props;

  const prefersReduced = useReducedMotion();
  const status: Status = error ? "error" : props.status ?? "default";
  const messageId = `${id}-message`;
  const showError = status === "error" && !!error;
  const message = showError ? error : helper;

  return (
    <div>
      <div className="mb-1.5 flex items-baseline justify-between gap-3">
        <label
          htmlFor={id}
          className="text-sm font-medium text-slate-800 dark:text-slate-200"
        >
          {label}
        </label>
        {optional && (
          <span className="text-xs font-normal text-slate-400 dark:text-slate-500">
            Optional
          </span>
        )}
      </div>

      <div className="relative">
        {leadingIcon && (
          <span
            className={cn(
              "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2",
              disabled
                ? "text-slate-300 dark:text-slate-600"
                : status === "error"
                  ? "text-rose-400 dark:text-rose-400"
                  : status === "success"
                    ? "text-emerald-500"
                    : "text-slate-400 dark:text-slate-500",
            )}
          >
            <span className="block h-5 w-5">{leadingIcon}</span>
          </span>
        )}

        <input
          ref={inputRef}
          id={id}
          type={type}
          value={value}
          disabled={disabled}
          placeholder={placeholder}
          autoComplete={autoComplete}
          inputMode={inputMode}
          onChange={(event) => onChange(event.target.value)}
          onBlur={onBlur}
          aria-invalid={showError || undefined}
          aria-describedby={message ? messageId : undefined}
          className={cn(
            "w-full rounded-xl border bg-white py-2.5 text-sm text-slate-900 shadow-sm outline-none transition-colors placeholder:text-slate-400",
            "dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-500",
            leadingIcon ? "pl-10" : "pl-3.5",
            trailing ? "pr-11" : "pr-3.5",
            disabled &&
              "cursor-not-allowed border-slate-200 bg-slate-100 text-slate-400 shadow-none dark:border-slate-800 dark:bg-slate-800/50 dark:text-slate-500",
            !disabled &&
              status === "default" &&
              "border-slate-300 hover:border-slate-400 focus:border-indigo-500 focus-visible:ring-2 focus-visible:ring-indigo-500/40 dark:border-slate-700 dark:hover:border-slate-600 dark:focus:border-indigo-400",
            !disabled &&
              status === "default" &&
              hoverPreview &&
              "border-slate-400 dark:border-slate-600",
            !disabled &&
              status === "error" &&
              "border-rose-400 focus:border-rose-500 focus-visible:ring-2 focus-visible:ring-rose-500/40 dark:border-rose-500/70 dark:focus:border-rose-400",
            !disabled &&
              status === "success" &&
              "border-emerald-400 focus:border-emerald-500 focus-visible:ring-2 focus-visible:ring-emerald-500/40 dark:border-emerald-500/70 dark:focus:border-emerald-400",
          )}
        />

        {trailing && (
          <span className="absolute right-1.5 top-1/2 -translate-y-1/2">
            {trailing}
          </span>
        )}
      </div>

      <div className="mt-1.5 min-h-[1.15rem] text-xs leading-tight">
        <AnimatePresence mode="wait" initial={false}>
          {message && (
            <motion.p
              key={message}
              id={messageId}
              role={showError ? "alert" : undefined}
              initial={prefersReduced ? { opacity: 0 } : { opacity: 0, y: -3 }}
              animate={{ opacity: 1, y: 0 }}
              exit={prefersReduced ? { opacity: 0 } : { opacity: 0, y: -3 }}
              transition={{ duration: 0.18, ease: "easeOut" }}
              className={cn(
                "flex items-center gap-1.5",
                showError
                  ? "text-rose-600 dark:text-rose-400"
                  : status === "success"
                    ? "text-emerald-600 dark:text-emerald-400"
                    : "text-slate-500 dark:text-slate-400",
              )}
            >
              {showError && (
                <AlertIcon className="inpbasic-pop h-3.5 w-3.5 shrink-0" />
              )}
              {!showError && status === "success" && (
                <CheckIcon className="inpbasic-pop h-3.5 w-3.5 shrink-0" />
              )}
              <span>{message}</span>
            </motion.p>
          )}
        </AnimatePresence>
      </div>
    </div>
  );
}

/* ---------------- showcase ---------------- */

export default function InpBasicSet() {
  const prefersReduced = useReducedMotion();
  const uid = useId();

  // --- state inspector ---
  const [inspectState, setInspectState] = useState<StateId>("default");
  const [demoValue, setDemoValue] = useState("");
  const demoRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (inspectState === "focus") demoRef.current?.focus();
  }, [inspectState]);

  const stateOptions: Array<{ id: StateId; label: string }> = [
    { id: "default", label: "Default" },
    { id: "hover", label: "Hover" },
    { id: "focus", label: "Focus" },
    { id: "disabled", label: "Disabled" },
    { id: "error", label: "Error" },
  ];

  const inspectorHelper: Record<StateId, string> = {
    default: "Resting state — neutral border, waiting for input.",
    hover: "Hover — the border darkens to signal the field is interactive.",
    focus: "Focus — an indigo ring and border appear while you type.",
    disabled: "Disabled — dimmed and non-interactive; skipped by the keyboard.",
    error: "",
  };

  // --- live validation form ---
  const [form, setForm] = useState({ name: "", email: "", password: "" });
  const [touched, setTouched] = useState<Record<FieldKey, boolean>>({
    name: false,
    email: false,
    password: false,
  });
  const [showPassword, setShowPassword] = useState(false);
  const [submitted, setSubmitted] = useState(false);

  const errors = {
    name: form.name.trim().length < 2 ? "Enter your full name." : "",
    email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)
      ? ""
      : "Enter a valid email address.",
    password: form.password.length < 8 ? "Use at least 8 characters." : "",
  };
  const isValid = !errors.name && !errors.email && !errors.password;

  const setField = (key: FieldKey, value: string) => {
    setForm((prev) => ({ ...prev, [key]: value }));
    setSubmitted(false);
  };
  const markTouched = (key: FieldKey) =>
    setTouched((prev) => ({ ...prev, [key]: true }));

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setTouched({ name: true, email: true, password: true });
    if (isValid) setSubmitted(true);
  };

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 dark:bg-slate-950 sm:px-6 sm:py-20 lg:py-24">
      <style>{`
        @keyframes inpbasic-pop {
          0% { transform: scale(.5); opacity: 0; }
          60% { transform: scale(1.12); }
          100% { transform: scale(1); opacity: 1; }
        }
        @keyframes inpbasic-pulse {
          0%, 100% { opacity: 1; }
          50% { opacity: .35; }
        }
        .inpbasic-pop { animation: inpbasic-pop .28s cubic-bezier(.34,1.56,.64,1) both; }
        .inpbasic-pulse { animation: inpbasic-pulse 2s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .inpbasic-pop, .inpbasic-pulse { animation: none !important; }
        }
      `}</style>

      <div
        aria-hidden="true"
        className="pointer-events-none absolute -top-24 left-1/2 h-72 w-72 -translate-x-1/2 rounded-full bg-indigo-400/15 blur-3xl dark:bg-indigo-500/10"
      />

      <div className="relative mx-auto max-w-4xl">
        <header className="mx-auto max-w-2xl text-center">
          <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300">
            <span className="inpbasic-pulse inline-block h-1.5 w-1.5 rounded-full bg-indigo-500" />
            Inputs · Text fields
          </span>
          <h2 className="mt-4 text-2xl font-semibold tracking-tight text-slate-900 dark:text-white sm:text-3xl">
            Text field states, done right
          </h2>
          <p className="mt-3 text-sm text-slate-600 dark:text-slate-400 sm:text-base">
            Default, hover, focus, disabled and error — every state is a real,
            accessible control with visible focus rings and inline validation
            messaging.
          </p>
        </header>

        <div className="mt-10 grid gap-6 lg:grid-cols-2">
          {/* state inspector */}
          <div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900/60 sm:p-8">
            <h3 className="text-base font-semibold text-slate-900 dark:text-white">
              State inspector
            </h3>
            <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
              Pick a state to see how the same field responds.
            </p>

            <div
              role="group"
              aria-label="Preview input state"
              className="mt-5 inline-flex flex-wrap gap-1 rounded-xl border border-slate-200 bg-slate-100 p-1 dark:border-slate-800 dark:bg-slate-900/80"
            >
              {stateOptions.map((option) => (
                <label key={option.id} className="relative cursor-pointer">
                  <input
                    type="radio"
                    name={`${uid}-state`}
                    value={option.id}
                    checked={inspectState === option.id}
                    onChange={() => setInspectState(option.id)}
                    className="peer sr-only"
                  />
                  <span className="block rounded-lg px-3 py-1.5 text-xs font-medium text-slate-600 transition-colors hover:text-slate-900 peer-checked:bg-white peer-checked:text-slate-900 peer-checked:shadow-sm peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 dark:text-slate-400 dark:hover:text-slate-100 dark:peer-checked:bg-slate-700 dark:peer-checked:text-white">
                    {option.label}
                  </span>
                </label>
              ))}
            </div>

            <div className="mt-6">
              <TextField
                id={`${uid}-demo`}
                inputRef={demoRef}
                label="Work email"
                type="email"
                inputMode="email"
                autoComplete="email"
                placeholder="you@company.com"
                leadingIcon={<MailIcon className="h-5 w-5" />}
                value={demoValue}
                onChange={setDemoValue}
                disabled={inspectState === "disabled"}
                hoverPreview={inspectState === "hover"}
                error={
                  inspectState === "error"
                    ? "That email is already registered."
                    : undefined
                }
                helper={
                  inspectState !== "error"
                    ? inspectorHelper[inspectState]
                    : undefined
                }
              />
            </div>
          </div>

          {/* live form */}
          <div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900/60 sm:p-8">
            <h3 className="text-base font-semibold text-slate-900 dark:text-white">
              Create your account
            </h3>
            <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
              Real-time validation with inline error text on blur.
            </p>

            <form onSubmit={handleSubmit} noValidate className="mt-5 space-y-4">
              <TextField
                id={`${uid}-name`}
                label="Full name"
                autoComplete="name"
                placeholder="Salman Farsi"
                leadingIcon={<UserIcon className="h-5 w-5" />}
                value={form.name}
                onChange={(value) => setField("name", value)}
                onBlur={() => markTouched("name")}
                error={touched.name && errors.name ? errors.name : undefined}
                status={
                  touched.name && !errors.name ? "success" : "default"
                }
                trailing={
                  touched.name && !errors.name ? <SuccessTick /> : undefined
                }
                helper={
                  touched.name && !errors.name
                    ? "Looks good."
                    : !touched.name
                      ? "As it should appear on your invoices."
                      : undefined
                }
              />

              <TextField
                id={`${uid}-email`}
                label="Work email"
                type="email"
                inputMode="email"
                autoComplete="email"
                placeholder="you@company.com"
                leadingIcon={<MailIcon className="h-5 w-5" />}
                value={form.email}
                onChange={(value) => setField("email", value)}
                onBlur={() => markTouched("email")}
                error={touched.email && errors.email ? errors.email : undefined}
                status={
                  touched.email && !errors.email ? "success" : "default"
                }
                trailing={
                  touched.email && !errors.email ? <SuccessTick /> : undefined
                }
                helper={
                  touched.email && !errors.email
                    ? "We'll send a magic link here."
                    : !touched.email
                      ? "We'll never share your address."
                      : undefined
                }
              />

              <TextField
                id={`${uid}-password`}
                label="Password"
                type={showPassword ? "text" : "password"}
                autoComplete="new-password"
                placeholder="At least 8 characters"
                leadingIcon={<LockIcon className="h-5 w-5" />}
                value={form.password}
                onChange={(value) => setField("password", value)}
                onBlur={() => markTouched("password")}
                error={
                  touched.password && errors.password
                    ? errors.password
                    : undefined
                }
                status={
                  touched.password && !errors.password ? "success" : "default"
                }
                helper={
                  touched.password && !errors.password
                    ? "Strong enough."
                    : !touched.password
                      ? "Mix letters, numbers and symbols."
                      : undefined
                }
                trailing={
                  <button
                    type="button"
                    onClick={() => setShowPassword((prev) => !prev)}
                    aria-label={
                      showPassword ? "Hide password" : "Show password"
                    }
                    aria-pressed={showPassword}
                    className="grid h-8 w-8 place-items-center rounded-lg text-slate-500 transition-colors hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-400 dark:hover:text-slate-100"
                  >
                    {showPassword ? (
                      <EyeOffIcon className="h-5 w-5" />
                    ) : (
                      <EyeIcon className="h-5 w-5" />
                    )}
                  </button>
                }
              />

              <button
                type="submit"
                className="inline-flex w-full items-center justify-center gap-2 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-visible: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-950"
              >
                Create account
              </button>

              <AnimatePresence initial={false}>
                {submitted && (
                  <motion.div
                    role="status"
                    initial={
                      prefersReduced ? { opacity: 0 } : { opacity: 0, y: -4 }
                    }
                    animate={{ opacity: 1, y: 0 }}
                    exit={{ opacity: 0 }}
                    transition={{ duration: 0.2, ease: "easeOut" }}
                    className="flex items-center gap-2 rounded-xl border border-emerald-200 bg-emerald-50 px-3.5 py-2.5 text-sm font-medium text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-300"
                  >
                    <CheckIcon className="h-4 w-4 shrink-0" />
                    Account created — confirm at{" "}
                    {form.email || "your inbox"}.
                  </motion.div>
                )}
              </AnimatePresence>
            </form>
          </div>
        </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 →