Login
Sign-in / sign-up card with animated mode switch and submit states.
Installation
npx shadcn@latest add @l8/loginUsage
import { LoginBlock } from '@/components/blocks/login';
export default function Page() {
return <LoginBlock />;
}Source
"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>
)
}