Motion+

Clerk: Conditional Field

A password field that animates into a sign-in form only when it's needed. The container animates its measured height with Motion's resize() util while the field fades in.

React

Source code

"use client"

import { useCallback, useEffect, useId, useState } from "react"
import { AnimatePresence, motion, MotionConfig, resize } from "motion/react"

/* A password field that animates in only when it's needed. In the Clerk
 * sign-in-or-up flow it appears when an entered email has no account yet,
 * prompting the user to create a password. The field's container animates
 * its measured height with Motion's resize() util, so the reveal stays
 * smooth regardless of any validation message rendered below it. */

function ConditionalField({
    open,
    label,
    error,
    ...props
}: React.ComponentProps<"input"> & {
    open: boolean
    label: string
    error?: string | null
}) {
    const [height, setHeight] = useState(0)
    const internalId = useId()
    const id = props.id || internalId

    const measureRef = useCallback((el: HTMLDivElement | null) => {
        if (!el) return
        return resize(el, (_, { height }) => setHeight(height))
    }, [])

    return (
        <motion.div animate={{ height: open ? height : 0 }} style={{ willChange: "height" }}>
            <div ref={measureRef} style={{ position: "relative" }}>
                <AnimatePresence mode="popLayout">
                    {open && (
                        <motion.div
                            key="password-field"
                            className="auth__field"
                            initial={{ opacity: 0, y: -8 }}
                            animate={{ opacity: 1, y: 0 }}
                            exit={{ opacity: 0, y: 0 }}
                            style={{ marginTop: "0.75rem" }}
                        >
                            <label htmlFor={id}>{label}</label>
                            <input autoFocus {...props} id={id} />
                            {error && <p className="auth__field-error">{error}</p>}
                        </motion.div>
                    )}
                </AnimatePresence>
            </div>
        </motion.div>
    )
}

export default function ClerkConditionalField() {
    const [email, setEmail] = useState("")
    const [password, setPassword] = useState("")
    const [showPassword, setShowPassword] = useState(false)
    const [done, setDone] = useState(false)

    useEffect(() => {
        if (!showPassword) return
        const handleKeyUp = (e: KeyboardEvent) => {
            if (e.key === "Escape") {
                e.preventDefault()
                setShowPassword(false)
            }
        }
        window.addEventListener("keyup", handleKeyUp)
        return () => window.removeEventListener("keyup", handleKeyUp)
    }, [showPassword])

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault()
        /* First submit with an email but no password reveals the password
         * field. In a real flow this branch is driven by Clerk returning
         * `form_identifier_not_found` for an unknown email. */
        if (email && !password) {
            setShowPassword(true)
        } else {
            setDone(true)
        }
    }

    return (
        <MotionConfig transition={{ type: "spring", bounce: 0.3, visualDuration: 0.4 }}>
            <div className="clerk-stage">
                <div className="auth__root">
                    <div className="auth__card-container">
                        <div 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>
                                <ConditionalField
                                    label="Password"
                                    name="password"
                                    id="password"
                                    open={showPassword}
                                    type="password"
                                    placeholder="Create a password"
                                    onBlur={({ target }) => {
                                        const hasPassword = target.value.length > 0
                                        setShowPassword(hasPassword)
                                    }}
                                    onChange={(e) => setPassword(e.target.value)}
                                    value={password}
                                    required
                                />
                                <button className="btn btn--primary" style={{ marginTop: "1.5rem" }} type="submit">
                                    {done ? "Welcome aboard" : "Submit"}
                                </button>
                            </form>
                        </div>
                    </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 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;
                        font-weight: 400;
                        opacity: 0.72;
                    }

                    .auth__field {
                        display: flex;
                        flex-direction: column;
                        gap: 0.5rem;
                    }
                    .auth__field-error {
                        color: #ff3300;
                        font-size: 0.75rem;
                        line-height: 1rem;
                    }
                    .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--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)); }
                `}</style>
            </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.