Web InnoventixFreeCode

Filled Input

Original · free

filled material-style inputs

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

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

/* ------------------------------------------------------------------ */
/* utils                                                               */
/* ------------------------------------------------------------------ */

function cx(...classes: Array<string | false | null | undefined>): string {
  return classes.filter(Boolean).join(" ");
}

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

type IconProps = { className?: string };

function UserIcon({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <circle cx="12" cy="8" r="3.6" />
      <path d="M4.5 20c.6-3.6 3.7-5.8 7.5-5.8s6.9 2.2 7.5 5.8" />
    </svg>
  );
}

function MailIcon({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <rect x="3" y="5" width="18" height="14" rx="2.5" />
      <path d="m3.5 7 8.5 6 8.5-6" />
    </svg>
  );
}

function LockIcon({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <rect x="4.5" y="10.5" width="15" height="9.5" rx="2.2" />
      <path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" />
      <circle cx="12" cy="15" r="1.2" />
    </svg>
  );
}

function EyeIcon({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <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 }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="M10.3 6.2A9.9 9.9 0 0 1 12 6c6 0 9.5 6 9.5 6a17 17 0 0 1-2.6 3.3M6.1 7.6A17 17 0 0 0 2.5 12S6 18 12 18a9.6 9.6 0 0 0 3.6-.7" />
      <path d="M9.9 9.9a3 3 0 0 0 4.2 4.2" />
      <path d="m4 4 16 16" />
    </svg>
  );
}

function PhoneIcon({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="M5 4h3.2l1.4 4-2 1.3a11 11 0 0 0 5.1 5.1l1.3-2 4 1.4V18a2 2 0 0 1-2.2 2A15.5 15.5 0 0 1 3 6.2 2 2 0 0 1 5 4Z" />
    </svg>
  );
}

function ChevronDownIcon({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.9} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="m6 9 6 6 6-6" />
    </svg>
  );
}

function SearchIcon({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <circle cx="11" cy="11" r="6.5" />
      <path d="m20 20-3.6-3.6" />
    </svg>
  );
}

function CloseIcon({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="m6 6 12 12M18 6 6 18" />
    </svg>
  );
}

function AlertIcon({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.9} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <circle cx="12" cy="12" r="9" />
      <path d="M12 7.5v5" />
      <circle cx="12" cy="16" r="0.6" fill="currentColor" />
    </svg>
  );
}

function CheckIcon({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="m5 12.5 4.5 4.5L19 7.5" />
    </svg>
  );
}

/* ------------------------------------------------------------------ */
/* shared tone helpers                                                 */
/* ------------------------------------------------------------------ */

function toneText(invalid: boolean, focused: boolean): string {
  if (invalid) return "text-rose-600 dark:text-rose-400";
  if (focused) return "text-indigo-600 dark:text-indigo-400";
  return "text-zinc-500 dark:text-zinc-400";
}

function toneBorder(invalid: boolean, focused: boolean): string {
  if (invalid) return "border-rose-500 dark:border-rose-400";
  if (focused) return "border-indigo-600 dark:border-indigo-400";
  return "border-zinc-400 dark:border-zinc-600";
}

const groupBase =
  "group relative flex w-full items-center rounded-t-lg border-b bg-zinc-100 transition-colors duration-150 has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-indigo-500/40 dark:bg-zinc-800/60";

/* ------------------------------------------------------------------ */
/* filled text field                                                   */
/* ------------------------------------------------------------------ */

type FilledFieldProps = {
  label: string;
  value: string;
  onChange: (value: string) => void;
  type?: string;
  placeholder?: string;
  helper?: string;
  error?: string;
  leading?: ReactNode;
  trailing?: ReactNode;
  disabled?: boolean;
  readOnly?: boolean;
  maxLength?: number;
  autoComplete?: string;
  inputMode?: "text" | "email" | "tel" | "numeric" | "search" | "url";
  name?: string;
};

function FilledField({
  label,
  value,
  onChange,
  type = "text",
  placeholder,
  helper,
  error,
  leading,
  trailing,
  disabled = false,
  readOnly = false,
  maxLength,
  autoComplete,
  inputMode,
  name,
}: FilledFieldProps) {
  const reduce = useReducedMotion();
  const uid = useId();
  const inputId = `${uid}-field`;
  const descId = `${uid}-desc`;
  const [focused, setFocused] = useState(false);

  const invalid = Boolean(error);
  const floated = focused || value.length > 0;
  const describe = error ?? helper;

  const descMotion = reduce
    ? { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }
    : { initial: { opacity: 0, y: -4 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -4 } };

  return (
    <div className="w-full">
      <div className={cx(groupBase, toneBorder(invalid, focused), disabled ? "opacity-60" : "hover:bg-zinc-200/70 dark:hover:bg-zinc-800")}>
        {leading ? (
          <span className={cx("pointer-events-none absolute left-3 top-1/2 -translate-y-1/2", toneText(invalid, focused))}>{leading}</span>
        ) : null}

        <input
          id={inputId}
          name={name}
          type={type}
          value={value}
          onChange={(e) => onChange(e.target.value)}
          onFocus={() => setFocused(true)}
          onBlur={() => setFocused(false)}
          placeholder={floated ? placeholder : undefined}
          disabled={disabled}
          readOnly={readOnly}
          maxLength={maxLength}
          autoComplete={autoComplete}
          inputMode={inputMode}
          aria-invalid={invalid ? true : undefined}
          aria-describedby={describe ? descId : undefined}
          className={cx(
            "peer h-14 w-full bg-transparent pb-1 pt-5 text-base text-zinc-900 caret-indigo-600 outline-none placeholder:text-zinc-400 disabled:cursor-not-allowed dark:text-zinc-100 dark:caret-indigo-400 dark:placeholder:text-zinc-500",
            leading ? "pl-11" : "pl-4",
            trailing ? "pr-12" : "pr-4",
          )}
        />

        <label
          htmlFor={inputId}
          className={cx(
            "pointer-events-none absolute origin-left transition-all duration-200 ease-out motion-reduce:transition-none",
            leading ? "left-11" : "left-4",
            floated ? "top-1.5 text-xs font-medium" : "top-1/2 -translate-y-1/2 text-base",
            toneText(invalid, focused),
          )}
        >
          {label}
        </label>

        {trailing ? <span className="absolute right-2 top-1/2 -translate-y-1/2">{trailing}</span> : null}

        <span
          aria-hidden="true"
          className={cx(
            "pointer-events-none absolute inset-x-0 bottom-0 h-0.5 origin-center transition-transform duration-200 ease-out motion-reduce:transition-none peer-focus:scale-x-100",
            invalid ? "scale-x-100 bg-rose-500 dark:bg-rose-400" : "scale-x-0 bg-indigo-600 dark:bg-indigo-400",
          )}
        />
      </div>

      <div className="mt-1.5 flex items-start justify-between gap-3 px-1">
        <div className="min-h-[1rem] text-xs leading-4">
          <AnimatePresence mode="wait" initial={false}>
            {error ? (
              <motion.p key="err" id={descId} className="flex items-center gap-1 text-rose-600 dark:text-rose-400" transition={{ duration: 0.18, ease: "easeOut" }} {...descMotion}>
                <AlertIcon className="h-3.5 w-3.5 shrink-0" />
                {error}
              </motion.p>
            ) : helper ? (
              <motion.p key="help" id={descId} className="text-zinc-500 dark:text-zinc-400" transition={{ duration: 0.18, ease: "easeOut" }} {...descMotion}>
                {helper}
              </motion.p>
            ) : null}
          </AnimatePresence>
        </div>
        {typeof maxLength === "number" ? (
          <span className="shrink-0 tabular-nums text-xs text-zinc-400 dark:text-zinc-500">
            {value.length}/{maxLength}
          </span>
        ) : null}
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* filled textarea                                                     */
/* ------------------------------------------------------------------ */

type FilledTextareaProps = {
  label: string;
  value: string;
  onChange: (value: string) => void;
  placeholder?: string;
  helper?: string;
  maxLength?: number;
  name?: string;
};

function FilledTextarea({ label, value, onChange, placeholder, helper, maxLength, name }: FilledTextareaProps) {
  const uid = useId();
  const inputId = `${uid}-area`;
  const descId = `${uid}-desc`;
  const [focused, setFocused] = useState(false);
  const floated = focused || value.length > 0;

  return (
    <div className="w-full">
      <div className={cx(groupBase, "items-stretch", toneBorder(false, focused), "hover:bg-zinc-200/70 dark:hover:bg-zinc-800")}>
        <textarea
          id={inputId}
          name={name}
          value={value}
          onChange={(e) => onChange(e.target.value)}
          onFocus={() => setFocused(true)}
          onBlur={() => setFocused(false)}
          placeholder={floated ? placeholder : undefined}
          maxLength={maxLength}
          aria-describedby={helper ? descId : undefined}
          rows={4}
          className="peer min-h-28 w-full resize-y bg-transparent px-4 pb-2 pt-7 text-base leading-relaxed text-zinc-900 caret-indigo-600 outline-none placeholder:text-zinc-400 dark:text-zinc-100 dark:caret-indigo-400 dark:placeholder:text-zinc-500"
        />

        <label
          htmlFor={inputId}
          className={cx(
            "pointer-events-none absolute left-4 origin-left transition-all duration-200 ease-out motion-reduce:transition-none",
            floated ? "top-1.5 text-xs font-medium" : "top-5 text-base",
            toneText(false, focused),
          )}
        >
          {label}
        </label>

        <span
          aria-hidden="true"
          className={cx(
            "pointer-events-none absolute inset-x-0 bottom-0 h-0.5 origin-center scale-x-0 bg-indigo-600 transition-transform duration-200 ease-out motion-reduce:transition-none peer-focus:scale-x-100 dark:bg-indigo-400",
          )}
        />
      </div>

      <div className="mt-1.5 flex items-start justify-between gap-3 px-1">
        {helper ? (
          <p id={descId} className="text-xs text-zinc-500 dark:text-zinc-400">
            {helper}
          </p>
        ) : (
          <span />
        )}
        {typeof maxLength === "number" ? (
          <span className="shrink-0 tabular-nums text-xs text-zinc-400 dark:text-zinc-500">
            {value.length}/{maxLength}
          </span>
        ) : null}
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* filled select                                                       */
/* ------------------------------------------------------------------ */

type FilledSelectProps = {
  label: string;
  value: string;
  onChange: (value: string) => void;
  options: readonly string[];
  helper?: string;
  name?: string;
};

function FilledSelect({ label, value, onChange, options, helper, name }: FilledSelectProps) {
  const uid = useId();
  const inputId = `${uid}-select`;
  const descId = `${uid}-desc`;
  const [focused, setFocused] = useState(false);
  const floated = focused || value.length > 0;

  return (
    <div className="w-full">
      <div className={cx(groupBase, toneBorder(false, focused), "hover:bg-zinc-200/70 dark:hover:bg-zinc-800")}>
        <select
          id={inputId}
          name={name}
          value={value}
          onChange={(e) => onChange(e.target.value)}
          onFocus={() => setFocused(true)}
          onBlur={() => setFocused(false)}
          aria-describedby={helper ? descId : undefined}
          className="peer h-14 w-full appearance-none bg-transparent pb-1 pl-4 pr-11 pt-5 text-base text-zinc-900 outline-none dark:text-zinc-100"
        >
          <option value="" hidden aria-label="No selection" />
          {options.map((opt) => (
            <option key={opt} value={opt} className="text-zinc-900">
              {opt}
            </option>
          ))}
        </select>

        <label
          htmlFor={inputId}
          className={cx(
            "pointer-events-none absolute left-4 origin-left transition-all duration-200 ease-out motion-reduce:transition-none",
            floated ? "top-1.5 text-xs font-medium" : "top-1/2 -translate-y-1/2 text-base",
            toneText(false, focused),
          )}
        >
          {label}
        </label>

        <span className={cx("pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 transition-transform duration-200 motion-reduce:transition-none", focused && "rotate-180", toneText(false, focused))}>
          <ChevronDownIcon className="h-5 w-5" />
        </span>

        <span
          aria-hidden="true"
          className="pointer-events-none absolute inset-x-0 bottom-0 h-0.5 origin-center scale-x-0 bg-indigo-600 transition-transform duration-200 ease-out motion-reduce:transition-none peer-focus:scale-x-100 dark:bg-indigo-400"
        />
      </div>

      {helper ? (
        <p id={descId} className="mt-1.5 px-1 text-xs text-zinc-500 dark:text-zinc-400">
          {helper}
        </p>
      ) : null}
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* keyframes                                                           */
/* ------------------------------------------------------------------ */

const KEYFRAMES = `
@keyframes inpfilled-pop {
  0% { transform: scale(0.4); opacity: 0; }
  60% { transform: scale(1.12); }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes inpfilled-blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}
.inpfilled-pop { animation: inpfilled-pop 0.4s ease-out both; }
.inpfilled-blink { animation: inpfilled-blink 1.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .inpfilled-pop, .inpfilled-blink { animation: none !important; }
}
`;

/* ------------------------------------------------------------------ */
/* demo                                                                */
/* ------------------------------------------------------------------ */

const COMPANY_SIZES = ["Just me", "2–10 people", "11–50 people", "51–200 people", "201–500 people", "500+ people"] as const;
const MESSAGE_MAX = 240;

export default function FilledInputs() {
  const reduce = useReducedMotion();

  const [fullName, setFullName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [phone, setPhone] = useState("");
  const [companySize, setCompanySize] = useState("");
  const [message, setMessage] = useState("");
  const [showPassword, setShowPassword] = useState(false);
  const [attempted, setAttempted] = useState(false);
  const [submitted, setSubmitted] = useState(false);

  const [search, setSearch] = useState("Filled text fields");

  const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  const emailError = email.length > 0 && !emailValid ? "Enter a valid address like alex@studio.com" : attempted && email.length === 0 ? "Email is required" : undefined;
  const passwordError = password.length > 0 && password.length < 8 ? "Use at least 8 characters" : attempted && password.length === 0 ? "Choose a password" : undefined;
  const nameError = attempted && fullName.trim().length === 0 ? "Your name is required" : undefined;

  const bannerMotion = reduce
    ? { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }
    : { initial: { opacity: 0, y: 8, scale: 0.98 }, animate: { opacity: 1, y: 0, scale: 1 }, exit: { opacity: 0, y: -8 } };

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-zinc-50 to-white px-4 py-16 sm:px-6 sm:py-24 dark:from-zinc-950 dark:to-zinc-900">
      <style>{KEYFRAMES}</style>

      <div className="mx-auto w-full max-w-5xl">
        {/* header */}
        <div className="mb-10 max-w-2xl">
          <span className="mb-3 inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white/70 px-3 py-1 text-xs font-medium text-zinc-600 dark:border-zinc-800 dark:bg-zinc-900/70 dark:text-zinc-300">
            <span className="inpfilled-blink h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden="true" />
            Filled inputs · Material style
          </span>
          <h2 className="text-3xl font-semibold tracking-tight text-zinc-900 sm:text-4xl dark:text-zinc-50">Text fields that keep their label in view</h2>
          <p className="mt-3 text-base text-zinc-600 dark:text-zinc-400">
            A tinted surface, an animated active indicator, and a floating label that lifts out of the way as you type. Every field is fully controlled, keyboard navigable, and labelled for screen readers.
          </p>
        </div>

        <div className="grid gap-6 lg:grid-cols-5">
          {/* main form */}
          <form
            noValidate
            onSubmit={(e) => {
              e.preventDefault();
              setAttempted(true);
              if (fullName.trim().length > 0 && emailValid && password.length >= 8) {
                setSubmitted(true);
              }
            }}
            className="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm sm:p-8 lg:col-span-3 dark:border-zinc-800 dark:bg-zinc-900"
          >
            <h3 className="text-lg font-semibold text-zinc-900 dark:text-zinc-50">Create your workspace</h3>
            <p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">Takes about a minute. No card required.</p>

            <AnimatePresence initial={false}>
              {submitted ? (
                <motion.div
                  key="banner"
                  transition={{ duration: 0.24, ease: "easeOut" }}
                  {...bannerMotion}
                  className="mt-5 flex items-start gap-3 rounded-xl border border-emerald-300 bg-emerald-50 p-4 dark:border-emerald-800/70 dark:bg-emerald-950/40"
                >
                  <span className="inpfilled-pop grid h-8 w-8 shrink-0 place-items-center rounded-full bg-emerald-600 text-white dark:bg-emerald-500">
                    <CheckIcon className="h-5 w-5" />
                  </span>
                  <div className="min-w-0 flex-1">
                    <p className="text-sm font-semibold text-emerald-800 dark:text-emerald-200">Workspace request received</p>
                    <p className="mt-0.5 text-sm text-emerald-700/90 dark:text-emerald-300/80">
                      Thanks {fullName.trim() || "there"} — we&apos;ll email a confirmation link to {email || "your inbox"} within one business day.
                    </p>
                  </div>
                  <button
                    type="button"
                    onClick={() => setSubmitted(false)}
                    className="shrink-0 rounded-lg px-2 py-1 text-xs font-medium text-emerald-800 underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 dark:text-emerald-200"
                  >
                    Edit details
                  </button>
                </motion.div>
              ) : null}
            </AnimatePresence>

            <div className="mt-6 grid gap-6">
              <FilledField
                label="Full name"
                name="name"
                value={fullName}
                onChange={setFullName}
                placeholder="Alex Chen"
                autoComplete="name"
                leading={<UserIcon className="h-5 w-5" />}
                helper="As it appears on official documents"
                error={nameError}
              />

              <FilledField
                label="Work email"
                name="email"
                type="email"
                inputMode="email"
                value={email}
                onChange={setEmail}
                placeholder="alex@studio.com"
                autoComplete="email"
                leading={<MailIcon className="h-5 w-5" />}
                helper="We'll send a confirmation link here"
                error={emailError}
              />

              <FilledField
                label="Password"
                name="password"
                type={showPassword ? "text" : "password"}
                value={password}
                onChange={setPassword}
                autoComplete="new-password"
                leading={<LockIcon className="h-5 w-5" />}
                helper="At least 8 characters"
                error={passwordError}
                trailing={
                  <button
                    type="button"
                    onClick={() => setShowPassword((s) => !s)}
                    aria-label={showPassword ? "Hide password" : "Show password"}
                    aria-pressed={showPassword}
                    className="grid h-9 w-9 place-items-center rounded-full text-zinc-500 transition hover:bg-zinc-200 hover:text-zinc-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:hover:text-zinc-200"
                  >
                    {showPassword ? <EyeOffIcon className="h-5 w-5" /> : <EyeIcon className="h-5 w-5" />}
                  </button>
                }
              />

              <div className="grid gap-6 sm:grid-cols-2">
                <FilledField
                  label="Phone"
                  name="phone"
                  type="tel"
                  inputMode="tel"
                  value={phone}
                  onChange={setPhone}
                  placeholder="+1 (555) 018-2043"
                  autoComplete="tel"
                  leading={<PhoneIcon className="h-5 w-5" />}
                  helper="Optional — for account recovery"
                />

                <FilledSelect label="Company size" name="company_size" value={companySize} onChange={setCompanySize} options={COMPANY_SIZES} helper="Helps us tailor onboarding" />
              </div>

              <FilledTextarea
                label="What are you building?"
                name="message"
                value={message}
                onChange={setMessage}
                placeholder="Tell us about your project, timeline, and the team you're inviting…"
                maxLength={MESSAGE_MAX}
                helper="Share context so we can route your request"
              />

              <div className="flex flex-col gap-3 pt-1 sm:flex-row sm:items-center sm:justify-between">
                <p className="text-xs text-zinc-500 dark:text-zinc-400">
                  By continuing you agree to the <span className="font-medium text-zinc-700 dark:text-zinc-300">Terms</span> and <span className="font-medium text-zinc-700 dark:text-zinc-300">Privacy Policy</span>.
                </p>
                <button
                  type="submit"
                  className="inline-flex items-center justify-center gap-2 rounded-xl bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition 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:scale-[0.98] motion-reduce:active:scale-100 dark:focus-visible:ring-offset-zinc-900"
                >
                  Create workspace
                </button>
              </div>
            </div>
          </form>

          {/* sidebar: states */}
          <aside className="flex flex-col gap-6 lg:col-span-2">
            <div className="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
              <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-50">Field states</h3>
              <p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">The same filled anatomy across common variants.</p>

              <div className="mt-5 grid gap-6">
                <FilledField
                  label="Search components"
                  value={search}
                  onChange={setSearch}
                  inputMode="search"
                  leading={<SearchIcon className="h-5 w-5" />}
                  helper="Type to filter — includes a clear button"
                  trailing={
                    search.length > 0 ? (
                      <button
                        type="button"
                        onClick={() => setSearch("")}
                        aria-label="Clear search"
                        className="grid h-9 w-9 place-items-center rounded-full text-zinc-500 transition hover:bg-zinc-200 hover:text-zinc-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:hover:text-zinc-200"
                      >
                        <CloseIcon className="h-4 w-4" />
                      </button>
                    ) : undefined
                  }
                />

                <FilledField label="Workspace plan" value="Team — 25 seats" onChange={() => {}} disabled helper="Managed by your admin" />

                <FilledField label="Invoice reference" value="INV-2026-0714" onChange={() => {}} readOnly helper="Copied from billing" />
              </div>
            </div>

            <div className="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
              <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-50">Accessibility built in</h3>
              <ul className="mt-3 space-y-2 text-sm text-zinc-600 dark:text-zinc-400">
                <li className="flex items-start gap-2">
                  <CheckIcon className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600 dark:text-emerald-400" />
                  Real <code className="rounded bg-zinc-100 px-1 text-xs text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">&lt;label&gt;</code> tied to each control
                </li>
                <li className="flex items-start gap-2">
                  <CheckIcon className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600 dark:text-emerald-400" />
                  Errors announced via <code className="rounded bg-zinc-100 px-1 text-xs text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">aria-describedby</code>
                </li>
                <li className="flex items-start gap-2">
                  <CheckIcon className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600 dark:text-emerald-400" />
                  Visible focus rings and reduced-motion fallbacks
                </li>
              </ul>
            </div>
          </aside>
        </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 →