How to build a social login page in React with Framer Motion
A React social login page renders a list of OAuth provider buttons with a per-item staggered entrance animation via Framer Motion, then an optional email fallback separated by a divider. The whole card fades in on mount with a spring-like ease curve.
- Stack: React 18 + Framer Motion 11 + Tailwind v4 + Lucide React, ~80 lines.
- Entrance animation: card fades up (opacity 0→1, y 20→0, 600ms) plus per-button stagger of 50ms each.
- Fully themeable via CSS custom properties; no hardcoded colors anywhere.
- Accessible: the email input uses type="email", the form prevents default submission, buttons are native <button> elements.
- Provider list is data-driven, pass any array of { name, icon } to render new buttons without touching JSX.
Auth Social Login is a centered React authentication card designed for apps that rely on OAuth providers as the primary sign-in path. It renders each provider as its own button, animates them in with a light stagger, then offers an email input below the fold for users who prefer not to link a social account. The layout is self-contained and themeable, drop it into any design system.
Anatomy
The outer motion.div is the animated card wrapper (max-w-sm, centered). Inside, a header holds a branded monogram square (first letter of brandName), the page title, and an optional subtitle. Below it, a vertical stack of motion.button elements represents the OAuth providers. When showEmailFallback is true, a flex divider labeled "ou par email" separates the provider list from a row containing an email input and an ArrowRight submit button.
How it works
Two Framer Motion layers handle the animation. The card wrapper animates once on mount: initial={{ opacity: 0, y: 20 }} → animate={{ opacity: 1, y: 0 }} over 600ms using a cubic-bezier(0.16, 1, 0.3, 1) ease, the same spring-like out-expo curve favored by Linear and Vercel. Each provider button gets its own motion.button with a 30ms transition and a delay of i * 50ms, creating the stagger without any orchestration API. No scroll triggers or gestures; the animation fires exactly once on mount.
How to build it in React
Define the Provider interface and props
Create a Provider type with name and icon fields. Expose providers as an array prop alongside brandName, title, subtitle and showEmailFallback. This keeps the component purely presentational, the parent handles OAuth redirects.
interface Provider { name: string; icon: string; } interface AuthSocialLoginProps { providers?: Provider[]; showEmailFallback?: boolean; brandName?: string; }Animate the card wrapper on mount
Wrap the entire card in a motion.div with the out-expo ease constant. This single animation covers the header, the provider list and the email fallback simultaneously, no need for nested orchestration.
const ease: [number, number, number, number] = [0.16, 1, 0.3, 1]; <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, ease }} >Stagger provider buttons with index delay
Map over the providers array and render each as a motion.button. Set delay: i * 0.05 in the transition. The first button appears almost instantly; each subsequent one follows 50ms later. Keep the per-button duration short (300ms) so the stagger feels snappy rather than slow.
{providers.map((provider, i) => ( <motion.button key={provider.name} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3, ease, delay: i * 0.05 }} > Continuer avec {provider.name} </motion.button> ))}Add the conditional email fallback
Gate the divider and email row behind showEmailFallback. The divider is two flex-1 hr-like divs flanking a centered label. The form uses onSubmit={(e) => e.preventDefault()}, actual submission logic lives in the parent.
{showEmailFallback && ( <> <div className="flex items-center gap-3 my-6"> <div className="flex-1 h-px" style={{ background: "var(--color-border)" }} /> <span className="text-xs">ou par email</span> <div className="flex-1 h-px" style={{ background: "var(--color-border)" }} /> </div> <form onSubmit={(e) => e.preventDefault()} className="flex gap-2"> <input type="email" placeholder="[email protected]" className="flex-1 px-4 py-3 rounded-lg text-sm" /> <button type="submit"><ArrowRight size={16} /></button> </form> </> )}
When to use it
Use this component on standalone auth routes where social login is the primary path and you want the interface to feel lightweight and focused. It fits SaaS tools, developer platforms, and any app where users already have Google or GitHub accounts. Skip it when you need a full-featured login form with password, MFA or magic links, those require additional fields and state the component deliberately does not include. On mobile the layout works as-is; just ensure the OAuth redirect flow is handled outside the component.
Used by
- Linear, Clean centered OAuth card with GitHub and Google as the two primary login options, no password field.
- Vercel, Social login with GitHub, GitLab and Bitbucket listed as provider buttons before any email option.
- Supabase, Auth UI library ships an out-of-the-box social provider button list with configurable providers and the same divider + email pattern.
- Clerk, Pre-built SignIn component stacks OAuth provider buttons above an email/password fallback, matching this exact layout.
FAQ
How do I wire up actual OAuth redirects?
The component is presentational, it exposes no onClick prop on provider buttons by default. In the parent, map over providers and attach an onClick that calls your auth library (NextAuth signIn, Supabase signInWithOAuth, Clerk OAuth flow, etc.).
Can I add a password field to this component?
The design intentionally separates social login from password auth. Add a password field by extending AuthSocialLoginProps and conditionally rendering an extra input below the email row, but at that point you are building a different pattern (combined login form) rather than a social-first card.
Why stagger buttons with index delay instead of Framer Motion's staggerChildren?
staggerChildren requires a variants object on both the parent and each child, which adds boilerplate. The delay: i * 0.05 pattern achieves the same visual result with less code and is easier to read at a glance when the list is dynamically generated.
Does the component handle loading state while OAuth redirects?
No. The current implementation is static, buttons have no disabled or loading state. Track loading per provider in the parent with a useState, pass it down, and conditionally render a spinner inside the button. The component's simplicity makes this straightforward to extend.