Motion+

Clerk: User Button

A custom Clerk user button built with Motion. The closed avatar expands into an account menu, using Motion's resize() util to animate the container to the menu's measured height.

React

Source code

"use client"

import { useEffect, useRef, useState } from "react"
import { AnimatePresence, motion } from "motion/react"

/* A custom Clerk <UserButton /> built with Motion. The closed button and
 * the open menu are two static layouts; Motion morphs between them via a
 * shared `layoutId` on each container (and a second on the avatar), so
 * there's no manual width/height animation to maintain. The menu content
 * fades in/out via AnimatePresence. In a real app the avatar and actions
 * come from Clerk's <UserAvatar /> and useClerk(); here they're mocked so
 * the demo runs standalone. */

const user = {
    fullName: "Ada Lovelace",
    email: "ada@motion.dev",
    initials: "AL",
}

const contentAnimations = {
    initial: { opacity: 0, filter: "blur(8px)" },
    animate: { opacity: 1, filter: "blur(0px)", transition: { delay: 0.15 } },
    exit: { opacity: 0, filter: "blur(8px)" },
}

const SPRING = { type: "spring", bounce: 0.15, visualDuration: 0.25 }

function Avatar({ large = false }: { large?: boolean }) {
    return (
        <motion.div
            layoutId="clerk-avatar"
            transition={SPRING}
            className={large ? "user-btn__avatar--large" : "user-btn__avatar"}
        >
            {user.initials}
        </motion.div>
    )
}

function SecuredByClerk() {
    return (
        <a href="https://clerk.com" target="_blank" rel="noreferrer" className="user-btn__clerk-credit">
            <span>Secured by</span>
            <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 14"><ellipse cx="7.889" cy="7" rx="2.187" ry="2.188" fill="currentColor"></ellipse><path d="M11.83 12.18a.415.415 0 0 1-.05.64A6.967 6.967 0 0 1 7.888 14a6.967 6.967 0 0 1-3.891-1.18.415.415 0 0 1-.051-.64l1.598-1.6a.473.473 0 0 1 .55-.074 3.92 3.92 0 0 0 1.794.431 3.92 3.92 0 0 0 1.792-.43.473.473 0 0 1 .551.074l1.599 1.598Z" fill="currentColor"></path><path opacity="0.5" d="M11.78 1.18a.415.415 0 0 1 .05.64l-1.598 1.6a.473.473 0 0 1-.55.073 3.937 3.937 0 0 0-5.3 5.3.473.473 0 0 1-.074.55L2.71 10.942a.415.415 0 0 1-.641-.051 7 7 0 0 1 9.71-9.71Z" fill="currentColor"></path><path fillRule="evenodd" clipRule="evenodd" d="M23.748 1.422c0-.06.05-.11.11-.11h1.64c.06 0 .11.05.11.11v11.156a.11.11 0 0 1-.11.11h-1.64a.11.11 0 0 1-.11-.11V1.422Zm-2.315 8.9a.112.112 0 0 0-.15.004 2.88 2.88 0 0 1-.884.569c-.36.148-.747.222-1.137.219-.33.01-.658-.047-.965-.166a2.422 2.422 0 0 1-.817-.527c-.424-.432-.668-1.05-.668-1.785 0-1.473.98-2.48 2.45-2.48.394-.005.785.074 1.144.234.325.144.617.35.86.607.04.044.11.049.155.01l1.108-.959a.107.107 0 0 0 .01-.152c-.832-.93-2.138-1.412-3.379-1.412-2.499 0-4.27 1.686-4.27 4.166 0 1.227.44 2.26 1.182 2.99.743.728 1.801 1.157 3.022 1.157 1.53 0 2.763-.587 3.485-1.34a.107.107 0 0 0-.009-.155l-1.137-.98Zm13.212-1.14a.108.108 0 0 1-.107.096H28.79a.106.106 0 0 0-.104.132c.286 1.06 1.138 1.701 2.302 1.701a2.59 2.59 0 0 0 1.136-.236 2.55 2.55 0 0 0 .862-.645.08.08 0 0 1 .112-.01l1.155 1.006c.044.039.05.105.013.15-.698.823-1.828 1.42-3.38 1.42-2.386 0-4.185-1.651-4.185-4.162 0-1.232.424-2.264 1.13-2.994.373-.375.82-.67 1.314-.87a3.968 3.968 0 0 1 1.557-.285c2.419 0 3.983 1.701 3.983 4.05a6.737 6.737 0 0 1-.04.647Zm-5.924-1.524a.104.104 0 0 0 .103.133h3.821c.07 0 .123-.066.103-.134-.26-.901-.921-1.503-1.947-1.503a2.13 2.13 0 0 0-.88.16 2.1 2.1 0 0 0-.733.507 2.242 2.242 0 0 0-.467.837Zm11.651-3.172c.061-.001.11.048.11.109v1.837a.11.11 0 0 1-.117.109 7.17 7.17 0 0 0-.455-.024c-1.43 0-2.27 1.007-2.27 2.329v3.732a.11.11 0 0 1-.11.11h-1.64a.11.11 0 0 1-.11-.11v-7.87c0-.06.049-.109.11-.109h1.64c.06 0 .11.05.11.11v1.104a.011.011 0 0 0 .02.007c.64-.857 1.587-1.333 2.587-1.333l.125-.001Zm4.444 4.81a.035.035 0 0 1 .056.006l2.075 3.334a.11.11 0 0 0 .093.052h1.865a.11.11 0 0 0 .093-.168L46.152 7.93a.11.11 0 0 1 .012-.131l2.742-3.026a.11.11 0 0 0-.081-.183h-1.946a.11.11 0 0 0-.08.036l-3.173 3.458a.11.11 0 0 1-.19-.074V1.422a.11.11 0 0 0-.11-.11h-1.64a.11.11 0 0 0-.11.11v11.156c0 .06.05.11.11.11h1.64a.11.11 0 0 0 .11-.11v-1.755a.11.11 0 0 1 .03-.075l1.35-1.452Z" fill="currentColor"></path></svg>
        </a>
    )
}

function MenuItem({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {
    return (
        <div className="user-btn__menu-item" onClick={onClick}>
            {children}
        </div>
    )
}

function MenuContent({ onManage, onSignOut }: { onManage: () => void; onSignOut: () => void }) {
    return (
        <motion.div
            layoutId="clerk-userbtn"
            className="user-btn__open"
            style={{ borderRadius: 16 }}
            transition={SPRING}
        >
            <div className="user-btn__menu-header">
                <div style={{ alignSelf: "center" }}>
                    <Avatar large />
                </div>
                <motion.div
                    className="user-btn__menu-header-content"
                    initial={contentAnimations.initial}
                    animate={contentAnimations.animate}
                    exit={contentAnimations.exit}
                >
                    <p>{user.fullName}</p>
                    <p>{user.email}</p>
                </motion.div>
            </div>
            <motion.div
                className="user-btn__menu-content"
                initial={contentAnimations.initial}
                animate={contentAnimations.animate}
                exit={contentAnimations.exit}
            >
                <MenuItem onClick={onManage}>
                    <svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fillRule="evenodd" clipRule="evenodd" d="M6.559 2.536A.667.667 0 0 1 7.212 2h1.574a.667.667 0 0 1 .653.536l.22 1.101c.466.178.9.429 1.287.744l1.065-.36a.667.667 0 0 1 .79.298l.787 1.362a.666.666 0 0 1-.136.834l-.845.742c.079.492.079.994 0 1.486l.845.742a.666.666 0 0 1 .137.833l-.787 1.363a.667.667 0 0 1-.791.298l-1.065-.36c-.386.315-.82.566-1.286.744l-.22 1.101a.666.666 0 0 1-.654.536H7.212a.666.666 0 0 1-.653-.536l-.22-1.101a4.664 4.664 0 0 1-1.287-.744l-1.065.36a.666.666 0 0 1-.79-.298L2.41 10.32a.667.667 0 0 1 .136-.834l.845-.743a4.7 4.7 0 0 1 0-1.485l-.845-.742a.667.667 0 0 1-.137-.833l.787-1.363a.667.667 0 0 1 .791-.298l1.065.36c.387-.315.821-.566 1.287-.744l.22-1.101ZM7.999 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"></path></svg>
                    Manage account
                </MenuItem>
                <MenuItem onClick={onSignOut}>
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" fillRule="evenodd" clipRule="evenodd" d="M2.6 2.604A2.045 2.045 0 0 1 4.052 2h3.417c.544 0 1.066.217 1.45.604.385.387.601.911.601 1.458v.69c0 .413-.334.75-.746.75a.748.748 0 0 1-.745-.75v-.69a.564.564 0 0 0-.56-.562H4.051a.558.558 0 0 0-.56.563v7.875a.564.564 0 0 0 .56.562h3.417a.558.558 0 0 0 .56-.563v-.671c0-.415.333-.75.745-.75s.746.335.746.75v.671c0 .548-.216 1.072-.6 1.459a2.045 2.045 0 0 1-1.45.604H4.05a2.045 2.045 0 0 1-1.45-.604A2.068 2.068 0 0 1 2 11.937V4.064c0-.548.216-1.072.6-1.459Zm8.386 3.116a.743.743 0 0 1 1.055 0l1.74 1.75a.753.753 0 0 1 0 1.06l-1.74 1.75a.743.743 0 0 1-1.055 0 .753.753 0 0 1 0-1.06l.467-.47H5.858A.748.748 0 0 1 5.112 8c0-.414.334-.75.746-.75h5.595l-.467-.47a.753.753 0 0 1 0-1.06Z"></path></svg>
                    Sign out
                </MenuItem>
                <div style={{ marginTop: 4 }}>
                    <SecuredByClerk />
                </div>
            </motion.div>
        </motion.div>
    )
}

function UserButton({ onSignOut }: { onSignOut: () => void }) {
    const [open, setOpen] = useState(false)
    const rootRef = useRef<HTMLDivElement>(null)

    useEffect(() => {
        const handleKeyDown = (event: KeyboardEvent) => {
            if (event.key === "Escape") setOpen(false)
        }
        window.addEventListener("keydown", handleKeyDown)
        return () => window.removeEventListener("keydown", handleKeyDown)
    }, [])

    useEffect(() => {
        if (!open) return
        const handleClickOutside = (event: MouseEvent) => {
            if (rootRef.current && !rootRef.current.contains(event.target as Node)) setOpen(false)
        }
        document.addEventListener("mousedown", handleClickOutside)
        return () => document.removeEventListener("mousedown", handleClickOutside)
    }, [open])

    return (
        <div className="user-btn__root" ref={rootRef}>
            <AnimatePresence>
                {!open ? (
                    <motion.button
                        key="closed"
                        layoutId="clerk-userbtn"
                        className="user-btn__closed"
                        style={{ borderRadius: 99 }}
                        transition={SPRING}
                        onClick={() => setOpen(true)}
                        aria-label="Open account menu"
                    >
                        <Avatar />
                    </motion.button>
                ) : (
                    <MenuContent
                        key="open"
                        onManage={() => setOpen(false)}
                        onSignOut={() => {
                            setOpen(false)
                            onSignOut()
                        }}
                    />
                )}
            </AnimatePresence>
        </div>
    )
}

export default function ClerkUserButton() {
    /* Clerk's <Show when="signed-in"> only renders children for an
     * authenticated user. Mocked here as local state so signing out and
     * back in is part of the demo. */
    const [signedIn, setSignedIn] = useState(true)

    return (
        <div className="clerk-stage">
            <header className="clerk-app-header">
                <AnimatePresence mode="popLayout" initial={false}>
                    {signedIn ? (
                        <UserButton key="user-button" onSignOut={() => setSignedIn(false)} />
                    ) : (
                        <motion.button
                            key="sign-in"
                            className="btn btn--default"
                            onClick={() => setSignedIn(true)}
                            initial={{ opacity: 0, scale: 0.9 }}
                            animate={{ opacity: 1, scale: 1 }}
                            exit={{ opacity: 0, scale: 0.9 }}
                            transition={{ type: "spring", bounce: 0.3, visualDuration: 0.2 }}
                        >
                            Sign in
                        </motion.button>
                    )}
                </AnimatePresence>
            </header>

            <div className="clerk-stage__body">
                <p className="clerk-stage__hint">
                    {signedIn
                        ? "Open the account menu from the top left."
                        : "Signed out. Select sign in to return."}
                </p>
            </div>

            <style>{`
                .clerk-stage {
                    --surface: #f7f7f8;
                    /* examples.motion.dev colours <a> via --accent (id specificity);
                     * pin it so the "Secured by Clerk" credit keeps its muted tone. */
                    --accent: #6b7280;
                    color-scheme: light;
                    position: relative;
                    align-self: stretch;
                    flex: 1 1 auto;
                    width: 100%;
                    min-height: 100%;
                    overflow: hidden;
                    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; }
                .clerk-stage a { color: inherit; text-decoration: none; }

                .clerk-app-header {
                    position: absolute;
                    top: 0;
                    left: 0;
                    right: 0;
                    display: flex;
                    align-items: flex-start;
                    justify-content: flex-start;
                    padding: 1.5rem;
                    z-index: 50;
                }

                .clerk-stage__body {
                    position: absolute;
                    inset: 0;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    padding: 1.5rem;
                }
                .clerk-stage__hint {
                    margin: 0;
                    font-size: 0.875rem;
                    opacity: 0.5;
                    text-align: center;
                    max-width: 22rem;
                    text-wrap: balance;
                }

                .user-btn__root {
                    position: relative;
                    width: 2.5rem;
                    height: 2.5rem;
                }

                /* The two static layouts Motion morphs between via the shared
                 * layoutId. Both anchor top-left so the card expands down and
                 * to the right out of the button. */
                .user-btn__closed {
                    position: absolute;
                    top: 0;
                    left: 0;
                    z-index: 90;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    width: 2.5rem;
                    height: 2.5rem;
                    padding: 0;
                    border: 0;
                    overflow: hidden;
                    cursor: pointer;
                    background-color: var(--surface);
                    background-image: linear-gradient(to bottom, rgb(0 0 0 / 0), rgb(0 0 0 / 0.03));
                    box-shadow: 0 8px 8px -3px rgb(0 0 0 / 0.1), 0 2px 3px -2px rgb(0 0 0 / 0.1), 0 0 0 0.5px rgb(0 0 0 / 0.1), inset 0 0 0 1px rgb(255 255 255);
                }

                /* Both avatars are the same fixed size; their shared layoutId
                 * morphs position between the button and the menu header. */
                .user-btn__avatar,
                .user-btn__avatar--large {
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    flex: none;
                    width: 2.5rem;
                    height: 2.5rem;
                    border-radius: 99px;
                    background: linear-gradient(140deg, #6c47ff, #9b8cff);
                    color: #fff;
                    font-size: 0.8125rem;
                    font-weight: 600;
                    user-select: none;
                }
                .user-btn__avatar--large {
                    font-size: 0.875rem;
                }

                .user-btn__open {
                    position: absolute;
                    top: 0;
                    left: 0;
                    z-index: 90;
                    width: 216px;
                    padding: 0.125rem;
                    overflow: hidden;
                    background-color: var(--surface);
                    background-image: linear-gradient(to bottom, rgb(0 0 0 / 0), rgb(0 0 0 / 0.03));
                    box-shadow: 0 0px 32px -8px rgb(0 0 0 / 0.1), 0 16px 20px -6px rgb(0 0 0 / 0.08), 0 6px 7px -6px rgb(0 0 0 / 0.2), 0 0 0 0.5px rgb(0 0 0 / 0.1), inset 0 0 0 1px rgb(255 255 255);
                }

                .user-btn__menu-header {
                    display: flex;
                    flex-direction: column;
                    gap: 0.75rem;
                    padding: 1rem;
                }
                .user-btn__menu-header-content {
                    text-align: center;
                    align-self: center;
                }
                .user-btn__menu-header-content p:nth-child(1) {
                    font-size: 0.875rem;
                    line-height: 1rem;
                    font-weight: 500;
                }
                .user-btn__menu-header-content p:nth-child(2) {
                    font-size: 0.75rem;
                    line-height: 1rem;
                    font-weight: 400;
                    opacity: 0.72;
                }

                .user-btn__menu-content {
                    display: flex;
                    flex-direction: column;
                    gap: 0.125rem;
                    padding: 0.25rem;
                    border-radius: 1rem;
                    background-color: #fff;
                    box-shadow: inset 0 -1px 2px rgb(0 0 0 / 0.03), 0 0 0 0.5px rgb(0 0 0 / 0.06);
                }

                .user-btn__menu-item {
                    height: 2rem;
                    display: flex;
                    align-items: center;
                    gap: 0.5rem;
                    padding: 0 0.75rem;
                    user-select: none;
                    font-size: 0.8125rem;
                    line-height: 1rem;
                    position: relative;
                    overflow: hidden;
                    border-radius: 0.375rem;
                    cursor: pointer;
                }
                .user-btn__menu-item:first-child {
                    border-top-left-radius: 0.75rem;
                    border-top-right-radius: 0.75rem;
                }
                .user-btn__menu-item:last-child {
                    border-bottom-left-radius: 0.75rem;
                    border-bottom-right-radius: 0.75rem;
                }
                .user-btn__menu-item:hover { box-shadow: inset 0 0 0 0.5px rgb(0 0 0 / 0.06); }
                .user-btn__menu-item::after {
                    content: "";
                    position: absolute;
                    inset: 0;
                    pointer-events: none;
                    background-color: currentColor;
                    opacity: 0;
                }
                .user-btn__menu-item:hover::after { opacity: 0.05; }
                .user-btn__menu-item:active::after { opacity: 0.08; }
                .user-btn__menu-item svg { width: 1rem; height: 1rem; }

                .user-btn__clerk-credit {
                    height: 2rem;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    gap: 0.25rem;
                    padding: 0 0.75rem;
                    font-size: 0.8125rem;
                    line-height: 1rem;
                    font-weight: 400;
                    color: #6b7280;
                }
                .user-btn__clerk-credit:hover { --accent: #212126; color: #212126; }
                .user-btn__clerk-credit svg { height: 0.75rem; }

                .btn {
                    display: inline-flex;
                    align-items: center;
                    justify-content: center;
                    position: relative;
                    height: 2rem;
                    border: 0;
                    padding: 0 0.75rem;
                    background-color: var(--surface);
                    color: #212126;
                    border-radius: 0.5rem;
                    overflow: hidden;
                    font-size: 0.8125rem;
                    font-weight: 500;
                    cursor: pointer;
                    box-shadow: inset 0 12px 0 -11px rgb(255 255 255 / 0.06), 0 2px 3px rgb(0 0 0 / 0.1), 0 0 0 1px rgb(0 0 0 / 0.05);
                }
                .btn::after {
                    content: "";
                    position: absolute;
                    inset: 0;
                    pointer-events: none;
                    background: linear-gradient(to bottom, rgb(0 0 0 / 0.05), rgb(0 0 0 / 0) 60%);
                }
                .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)); }
            `}</style>
        </div>
    )
}

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.