Clerk: Card Stack
The verification step of a custom Clerk flow. Submitting the email stacks a one-time-code card up over the sign-in card, with AnimatePresence popLayout dimming the card behind it.
Source code
"use client"
import { useEffect, useState } from "react"
import { AnimatePresence, motion, MotionConfig } from "motion/react"
/* The verification step in a custom Clerk flow. Submitting the email
* "stacks" a verification card up over the sign-in card: the lower card
* dims and recedes while the OTP card springs in from below. In a real
* flow the email submit calls Clerk's signIn.emailCode.sendCode() and the
* code is verified with signIn.emailCode.verifyCode(). */
const SIGN_IN_VARIANTS = {
default: { opacity: 1, scale: 1, y: 0, x: "-50%" },
verifying: { opacity: 0.6, scale: 0.95, y: -10, x: "-50%" },
}
const VERIFY_VARIANTS = {
default: { opacity: 0, y: 100, x: "-50%" },
verifying: { opacity: 1, y: 0, x: "-50%" },
}
/* A minimal one-time-code input: a transparent field captures keystrokes
* over a row of slots. The guide uses the `input-otp` package; this is an
* inline stand-in so the example stays dependency-free. */
function OTPInput({ length = 6, autoFocus }: { length?: number; autoFocus?: boolean }) {
const [code, setCode] = useState("")
const activeIndex = Math.min(code.length, length - 1)
return (
<div className="otp">
<input
className="otp__input"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, length))}
inputMode="numeric"
autoComplete="one-time-code"
aria-label="Verification code"
autoFocus={autoFocus}
/>
<div className="otp__slots" aria-hidden="true">
{Array.from({ length }).map((_, i) => (
<div key={i} className="otp__slot" data-active={i === activeIndex}>
{code[i] ?? ""}
</div>
))}
</div>
</div>
)
}
function ResendButton(props: React.ComponentProps<"button">) {
const [countdown, setCountdown] = useState(30)
useEffect(() => {
const id = setInterval(() => setCountdown((prev) => (prev <= 0 ? prev : prev - 1)), 1000)
return () => clearInterval(id)
}, [])
return (
<button {...props} type="button" className="resend" disabled={countdown > 0}>
Didn't receive a code? Resend {countdown > 0 ? `(${countdown})` : ""}
</button>
)
}
export default function ClerkCardStack() {
const [email, setEmail] = useState("")
const [isVerifying, setIsVerifying] = useState(false)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setIsVerifying(true)
}
const handleReset = () => {
setIsVerifying(false)
setEmail("")
}
const handleSubmitVerify = (e: React.FormEvent) => {
e.preventDefault()
handleReset()
}
return (
<MotionConfig transition={{ type: "spring", bounce: 0.3, visualDuration: 0.4 }}>
<motion.div className="clerk-stage" animate={isVerifying ? "verifying" : "default"}>
<div className="auth__root">
<div className="auth__card-container">
<motion.div variants={SIGN_IN_VARIANTS} initial="default" className="auth__card">
<h3 className="auth__card-title">Welcome to Motion</h3>
<p className="auth__card-description">Enter your details to continue</p>
<form onSubmit={handleSubmit}>
<div className="auth__field">
<label htmlFor="email">Email</label>
<input
required
placeholder="Enter your email address"
type="email"
name="email"
id="email"
onChange={(e) => setEmail(e.target.value)}
value={email}
/>
</div>
<button className="btn btn--primary" style={{ marginTop: "1.5rem" }} type="submit">
Submit
</button>
</form>
</motion.div>
<AnimatePresence mode="popLayout">
{isVerifying && (
<motion.div
className="auth__card auth__card--overlay"
variants={VERIFY_VARIANTS}
initial="default"
animate="verifying"
exit="default"
>
<h3 className="auth__card-title">Verify your account</h3>
<p className="auth__card-description">
Enter the code sent to your email address
</p>
<form onSubmit={handleSubmitVerify}>
<div
className="auth__field"
style={{ width: "min-content", marginInline: "auto" }}
>
<OTPInput autoFocus length={6} />
</div>
<ResendButton style={{ marginInline: "auto", marginTop: "0.5rem" }} />
<button className="btn btn--primary" style={{ marginTop: "1.5rem" }} type="submit">
Verify
</button>
</form>
<button className="btn" onClick={handleReset} type="button" style={{ marginTop: "0.5rem" }}>
Start over
</button>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
<style>{`
.clerk-stage {
--surface: #f7f7f8;
color-scheme: light;
position: relative;
align-self: stretch;
flex: 1 1 auto;
width: 100%;
min-height: 100%;
overflow: auto;
background: #ededed;
color: #212126;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.clerk-stage, .clerk-stage * { box-sizing: border-box; }
.auth__root {
display: grid;
justify-content: center;
align-items: flex-start;
min-height: 100%;
padding: clamp(2rem, 14vh, 6rem) 1rem 2rem;
}
.auth__card-container {
position: relative;
grid-column: 1;
grid-row: 1;
}
.auth__card {
position: absolute;
left: 50%;
width: 25rem;
max-width: calc(100vw - 2rem);
transform: translateX(-50%);
padding: 2.5rem 2rem;
background-color: var(--surface);
border-radius: 2rem;
transform-origin: top center;
box-shadow:
inset 0 0 0 1px rgb(255 255 255),
inset 0 2px 4px 0 rgb(255 255 255),
inset 0 4px 10px 0 hsl(0 0 0 / 0.05),
0 4px 14px -10px hsl(0 0 0 / 0.4),
0 8px 28px -10px hsl(0 0 0 / 0.1),
0 0 0 1px rgb(0 0 0 / 0.06);
}
.auth__card--overlay { z-index: 10; }
.auth__card button { width: 100%; }
.auth__card form { margin-top: 2rem; }
.auth__card-title {
text-align: center;
font-size: 1.125rem;
line-height: 1.5rem;
font-weight: 600;
letter-spacing: -0.025em;
}
.auth__card-description {
text-align: center;
font-size: 0.875rem;
line-height: 1.25rem;
opacity: 0.72;
}
.auth__field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.auth__field label {
font-size: 0.8125rem;
line-height: 1rem;
font-weight: 500;
}
.auth__field input {
--border-color: rgb(0 0 0 / 0.1);
--ring: 0 0 0 0px rgb(0 0 0 / 0);
width: 100%;
border-radius: 0.375rem;
background-color: var(--surface);
padding: 0.375rem 0.75rem;
font-size: 0.8125rem;
line-height: 1.125rem;
color: #212126;
border: none;
outline: none;
box-shadow: 0 1px 3px -1px rgb(0 0 0 / 0.08), 0 0 0 1px var(--border-color), var(--ring);
}
.auth__field input::placeholder { color: rgb(0 0 0 / 0.4); }
.auth__field input:hover { --border-color: rgb(0 0 0 / 0.2); }
.auth__field input:focus-visible {
--border-color: rgb(0 0 0 / 0.2);
--ring: 0 0 0 3px rgb(0 0 0 / 0.06);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
height: 2rem;
border: 0;
padding: 0 0.75rem;
background-color: transparent;
color: #212126;
border-radius: 0.5rem;
overflow: hidden;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
}
.btn::after { content: ""; position: absolute; inset: 0; pointer-events: none; }
.btn:hover::after { background: linear-gradient(to bottom, rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.05)); }
.btn:active::after { background: linear-gradient(to bottom, rgb(0 0 0 / 0.08), rgb(0 0 0 / 0.08)); }
.btn--primary {
background-color: #212126;
color: #fff;
box-shadow: inset 0 12px 0 -11px rgb(255 255 255 / 0.12), 0 2px 3px rgb(0 0 0 / 0.2), 0 0 0 1px #212126;
}
.btn--primary::after { background: linear-gradient(to bottom, rgb(255 255 255 / 0.15), rgb(255 255 255 / 0) 60%); }
.btn--primary:hover::after { background: linear-gradient(to bottom, rgb(255 255 255 / 0.12), rgb(255 255 255 / 0.12)); }
.btn--primary:active::after { background: linear-gradient(to bottom, rgb(255 255 255 / 0.2), rgb(255 255 255 / 0.2)); }
.otp { position: relative; width: min-content; }
.otp__input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: text;
border: none;
background: transparent;
}
.otp__slots { display: flex; }
.otp__slot {
--border-color: rgb(0 0 0 / 0.1);
--ring: 0 0 0 0px rgb(0 0 0 / 0);
margin-inline-start: -1px;
position: relative;
display: flex;
width: 2.25rem;
height: 2.25rem;
align-items: center;
justify-content: center;
background-color: var(--surface);
font-size: 0.8125rem;
line-height: 1.125rem;
font-weight: 500;
box-shadow: 0 1px 3px -1px rgb(0 0 0 / 0.08), 0 0 0 0.5px var(--border-color), var(--ring);
}
.otp__slot[data-active="true"] {
--border-color: rgb(0 0 0 / 0.2);
--ring: 0 0 0 3px rgb(0 0 0 / 0.12);
z-index: 10;
}
.otp__slot:first-child {
border-top-left-radius: 0.375rem;
border-bottom-left-radius: 0.375rem;
margin-inline-start: 0;
}
.otp__slot:last-child {
border-top-right-radius: 0.375rem;
border-bottom-right-radius: 0.375rem;
}
.resend {
border: 0;
background-color: transparent;
display: block;
color: #212126;
opacity: 0.75;
font-size: 0.8125rem;
line-height: 1rem;
font-weight: 500;
cursor: pointer;
}
.resend:hover { opacity: 1; }
.resend[disabled] { opacity: 0.4; cursor: not-allowed; }
`}</style>
</motion.div>
</MotionConfig>
)
}Related examples
Latest in React
Motion+
Unlock all 400+ examples
- Source code for every Plus example.
- Provide examples direct to your agent via Motion's MCP.
- Lifetime access to new examples and APIs.







