Glass Verification Code Card

PreviousNext

Verification code entry with glassmorphic inputs, status messaging, and reduced-motion support

Docs
uitripledcomponent

Preview

Loading preview…
components/components/forms/glass-verification-code.tsx
"use client";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { motion, useReducedMotion } from "framer-motion";
import { FormEvent, useMemo, useState } from "react";

const CODE_LENGTH = 6;

export function GlassVerificationCodeCard() {
  const shouldReduceMotion = useReducedMotion();
  const [code, setCode] = useState(Array<string>(CODE_LENGTH).fill(""));
  const [status, setStatus] = useState<"idle" | "verified" | "error">("idle");

  const handleChange = (value: string, index: number) => {
    const sanitized = value.replace(/\D/g, "").slice(-1);
    setCode((prev) => {
      const updated = [...prev];
      updated[index] = sanitized;
      return updated;
    });
  };

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const hasEmpty = code.some((digit) => digit.trim().length === 0);
    setStatus(hasEmpty ? "error" : "verified");
  };

  const combinedCode = useMemo(() => code.join(""), [code]);

  const resetCode = () => {
    setCode(Array<string>(CODE_LENGTH).fill(""));
    setStatus("idle");
  };

  return (
    <motion.div
      initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 18 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{
        duration: 0.45,
        ease: shouldReduceMotion ? "linear" : [0.16, 1, 0.3, 1],
      }}
      className="group w-full max-w-md rounded-3xl overflow-hidden border border-border/60 bg-card/85 p-8 backdrop-blur-xl sm:p-10 relative"
      aria-labelledby="glass-verification-title"
    >
      <div
        aria-hidden="true"
        className="absolute inset-0 bg-gradient-to-br from-foreground/[0.04] via-transparent to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 -z-10"
      />
      <div className="mb-8 text-center">
        <div className="mx-auto inline-flex items-center gap-2 rounded-full border border-border/60 bg-white/5 px-3 py-1 text-xs uppercase tracking-[0.28em] text-muted-foreground">
          Verify Email
        </div>
        <h1
          id="glass-verification-title"
          className="mt-3 text-2xl font-semibold text-foreground sm:text-3xl"
        >
          Enter verification code
        </h1>
        <p className="mt-2 text-sm text-muted-foreground">
          We sent a 6-digit code to your inbox. Enter it below to confirm your
          email.
        </p>
      </div>

      <form className="space-y-6" onSubmit={handleSubmit}>
        <div className="flex justify-between gap-2">
          {code.map((digit, index) => (
            <Input
              key={index}
              type="text"
              inputMode="numeric"
              pattern="[0-9]*"
              maxLength={1}
              value={digit}
              onChange={(event) => handleChange(event.target.value, index)}
              className="h-14 w-full rounded-2xl border-border/60 bg-background/60 text-center text-lg font-semibold"
              aria-label={`Verification digit ${index + 1}`}
            />
          ))}
        </div>

        <Button
          type="submit"
          className="w-full rounded-full bg-primary px-6 py-3 text-primary-foreground shadow-[0_18px_55px_-30px_rgba(79,70,229,0.75)] transition-transform duration-300 hover:-translate-y-1"
        >
          Verify code
        </Button>
      </form>

      <motion.p
        role="status"
        initial={{ opacity: 0 }}
        animate={{ opacity: status === "verified" ? 1 : 0 }}
        className="mt-6 text-center text-xs text-primary/80"
      >
        Verification successful! Redirecting you now.
      </motion.p>
      <motion.p
        role="status"
        initial={{ opacity: 0 }}
        animate={{ opacity: status === "error" ? 1 : 0 }}
        className="mt-6 text-center text-xs text-rose-400/80"
      >
        Please fill every digit before verifying.
      </motion.p>

      <div className="mt-6 flex items-center justify-between text-xs text-muted-foreground">
        <button
          type="button"
          className="text-primary underline-offset-4 hover:underline"
          onClick={resetCode}
        >
          Resend code
        </button>
        <span aria-live="polite">Code: {combinedCode || "______"}</span>
      </div>
    </motion.div>
  );
}

Installation

npx shadcn@latest add @uitripled/glass-verification-code-card

Usage

import { GlassVerificationCodeCard } from "@/components/glass-verification-code-card"
<GlassVerificationCodeCard />