l8components

Login

Sign-in / sign-up card with animated mode switch and submit states.

or

Installation

npx shadcn@latest add @l8/login

Usage

import { LoginBlock } from '@/components/blocks/login';

export default function Page() {
  return <LoginBlock />;
}

Source

components/blocks/login.tsx
"use client"

import * as React from "react"
import { AnimatePresence, motion } from "motion/react"
import { CheckIcon, KeyRoundIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { Field, FieldGroup, FieldLabel, FieldSeparator } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Spinner } from "@/components/ui/spinner"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"

type Mode = "sign-in" | "sign-up"

export function LoginBlock() {
  const [mode, setMode] = React.useState<Mode>("sign-in")
  const [status, setStatus] = React.useState<"idle" | "loading" | "done">("idle")

  function submit(e: React.FormEvent) {
    e.preventDefault()
    setStatus("loading")
    // Demo only: fake a request.
    setTimeout(() => setStatus("done"), 1200)
    setTimeout(() => setStatus("idle"), 2800)
  }

  return (
    <section className="relative flex justify-center overflow-hidden px-6 py-16">
      <div aria-hidden className="absolute top-0 left-1/2 -z-0 size-96 -translate-x-1/2 rounded-full bg-primary/10 blur-3xl" />
      <motion.div
        layout
        initial={{ opacity: 0, y: 20, scale: 0.98 }}
        animate={{ opacity: 1, y: 0, scale: 1 }}
        transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
        className="relative w-full max-w-sm rounded-xl border bg-card/80 p-6 shadow-xl backdrop-blur"
      >
        <Tabs value={mode} onValueChange={(v) => setMode(v as Mode)}>
          <TabsList className="w-full">
            <TabsTrigger value="sign-in">Sign in</TabsTrigger>
            <TabsTrigger value="sign-up">Create account</TabsTrigger>
          </TabsList>
        </Tabs>
        <AnimatePresence mode="wait" initial={false}>
          <motion.form
            key={mode}
            onSubmit={submit}
            initial={{ opacity: 0, x: mode === "sign-up" ? 20 : -20 }}
            animate={{ opacity: 1, x: 0 }}
            exit={{ opacity: 0, x: mode === "sign-up" ? -20 : 20 }}
            transition={{ duration: 0.2 }}
            className="mt-6"
          >
            <FieldGroup>
              {mode === "sign-up" && (
                <Field>
                  <FieldLabel htmlFor="name">Name</FieldLabel>
                  <Input id="name" placeholder="Ada Lovelace" required />
                </Field>
              )}
              <Field>
                <FieldLabel htmlFor="email">Email</FieldLabel>
                <Input id="email" type="email" placeholder="you@company.com" required />
              </Field>
              <Field>
                <FieldLabel htmlFor="password">Password</FieldLabel>
                <Input id="password" type="password" required />
              </Field>
              <Button type="submit" size="lg" className="w-full rounded-full" disabled={status !== "idle"}>
                <AnimatePresence mode="wait" initial={false}>
                  <motion.span
                    key={status}
                    className="flex items-center gap-2"
                    initial={{ opacity: 0, y: 8 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={{ opacity: 0, y: -8 }}
                  >
                    {status === "loading" && <Spinner />}
                    {status === "done" && <CheckIcon />}
                    {status === "idle" ? (mode === "sign-in" ? "Sign in" : "Create account") : status === "loading" ? "Please wait" : "Welcome!"}
                  </motion.span>
                </AnimatePresence>
              </Button>
              <FieldSeparator>or</FieldSeparator>
              <Button type="button" variant="outline" size="lg" className="w-full rounded-full">
                <KeyRoundIcon data-icon="inline-start" />
                Continue with SSO
              </Button>
            </FieldGroup>
          </motion.form>
        </AnimatePresence>
      </motion.div>
    </section>
  )
}

On this page