Cómo crear un formulario de login centrado en React con Framer Motion
Un formulario de login React centrado agrupa email, contraseña y botones sociales opcionales en una sola tarjeta estrecha, centrada vertical y horizontalmente con Flexbox. Una animación fade-in de Framer Motion (opacity 0→1, y 20→0) durante 600ms con un ease spring personalizado da a la tarjeta una entrada pulida sin saltos al cargar la página.
- Stack: React + Framer Motion 11 + Tailwind v4, ~194 líneas, sin librería de iconos en runtime.
- Animación de entrada: motion.div con initial { opacity: 0, y: 20 } animado a { opacity: 1, y: 0 }, duración 0,6s, cubic-bezier personalizado [0.16, 1, 0.3, 1].
- Los botones sociales (Google + GitHub) y la casilla "recordarme" se activan mediante props booleanas, sin markup adicional cuando están desactivados.
- Totalmente adaptable al tema: todos los colores se leen desde CSS custom properties (--color-background, --color-accent, --color-border), compatible con los 7 presets del registry.
- Accesible: las etiquetas están asociadas a sus campos, el formulario usa un elemento <form> nativo, el botón de envío es type='submit'.
Auth Login Centered es una página de autenticación React independiente que centra una tarjeta de login compacta en el viewport. Combina un formulario de email y contraseña con atajos OAuth opcionales (Google, GitHub), una casilla de recordarme y un enlace de contraseña olvidada, todos los bloques que un producto SaaS necesita sin dependencias fijas a una librería de auth concreta.
Anatomía
La sección exterior se extiende a min-h-screen y usa Flexbox (items-center justify-center) para centrar la tarjeta interior. La tarjeta es un motion.div limitado a max-w-sm. Dentro: un bloque de marca (inicial en un cuadrado accent + h1 + subtítulo opcional), una fila social opcional (dos botones lado a lado separados del formulario por una línea horizontal con la etiqueta 'o'), luego el formulario con campo de email, campo de contraseña, una fila de recordarme/contraseña olvidada y el botón de envío. Un enlace de registro se sitúa debajo del formulario.
Cómo funciona
La entrada de la tarjeta es una sola transición de Framer Motion sobre el motion.div envolvente. El array de ease [0.16, 1, 0.3, 1] es una curva expo-out: acelera con fuerza al principio y luego desacelera hacia la posición de reposo, lo que da la sensación de que la tarjeta encaja en su sitio en vez de derivar. El desplazamiento y de 20px mantiene el recorrido corto para que la animación se lea como 'llegada' y no como un scroll reveal. Sin disparadores de scroll, sin IntersectionObserver, la animación se lanza de inmediato al montar.
Cómo crearlo en React
Centrar la maquetación de la página
Envuelve todo en una section con min-h-screen y centrado Flexbox. Esto gestiona el centrado vertical y horizontal sin altura fija ni posicionamiento absoluto, así la tarjeta permanece centrada incluso si el contenido se desborda en pantallas pequeñas.
<section className="min-h-screen flex items-center justify-center py-16 px-6" style={{ background: "var(--color-background)" }}>Animar la tarjeta al montar
Reemplaza la div interior por un motion.div y define initial/animate. La curva de ease personalizada da la sensación de pop-in. Mantén el desplazamiento y pequeño (20px) para que se lea como un aterrizaje, no como una caída.
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 }} className="w-full max-w-sm" >Condicionar el bloque social con una prop
Envuelve los botones sociales y el separador en un único test showSocial. Así el componente se renderiza limpiamente como formulario solo de email cuando OAuth no está disponible, sin espacios vacíos que compensar.
{showSocial && ( <> <div className="flex gap-3 mb-6"> <button style={{ border: "1px solid var(--color-border)" }}>Google</button> <button style={{ border: "1px solid var(--color-border)" }}>GitHub</button> </div> {/* divider */} </> )}Conectar los campos del formulario con tokens CSS
Usa CSS custom properties para todos los colores para que el formulario se adapte a cada preset de tema sin una sola clase condicional. El token background-alt aporta el fondo de input sutil que diferencia los campos de la superficie de la tarjeta.
<input type="email" style={{ background: "var(--color-background-alt)", color: "var(--color-foreground)", border: "1px solid var(--color-border)", }} />
Cuándo usarlo
Recurre a este layout siempre que la pantalla de login sea el destino principal: dashboards SaaS, herramientas internas, apps B2B. Funciona bien cuando la autenticación es un punto de fricción esperado y el usuario llega con intención de acceder. Evítalo en productos donde el login está enterrado dentro de un flujo (como un checkout), allí encaja mejor una modal o un formulario inline. La prop showSocial facilita quitar los botones OAuth cuando solo se admite auth por email y contraseña.
Usado por
- Linear, Login centrado minimalista con email y Google OAuth, separación clara entre los flujos social y de email.
- Vercel, Tarjeta de auth centrada con botones sociales de GitHub, GitLab y Bitbucket encima de un formulario de email, coincidiendo exactamente con el patrón de layout de este componente.
- Notion, Login centrado a una sola columna con OAuth de Google y Apple, luego una opción de email bajo un separador, la estructura apilada clásica que sigue este componente.
- Loom, Tarjeta de auth centrada con el logo de la marca arriba, botones de login social y un formulario de email debajo, la misma anatomía que este componente.
FAQ
¿Cómo conecto este formulario a una librería de auth real como NextAuth o Supabase?
Reemplaza el handler onSubmit (actualmente e.preventDefault()) por la llamada de inicio de sesión de tu librería. Para NextAuth: signIn('credentials', { email, password }). Para Supabase: supabase.auth.signInWithPassword({ email, password }). Los botones sociales siguen el mismo esquema: signIn('google') o supabase.auth.signInWithOAuth({ provider: 'google' }).
¿Puedo desactivar la animación de entrada para usuarios que prefieren menos movimiento?
Sí. Condiciona la configuración de la transición al hook useReducedMotion de Framer Motion. Cuando devuelve true, pon duration a 0 o elimina por completo las props initial/animate: initial={reducedMotion ? false : { opacity: 0, y: 20 }} con transition={{ duration: reducedMotion ? 0 : 0.6, ease }}.
¿Cómo añado validación al formulario?
El formulario es no controlado por defecto. Cambia los inputs por versiones controladas con useState, luego valida al enviar o al blur. React Hook Form se integra limpiamente: registra cada input con register y llama a handleSubmit, que reemplaza el onSubmit actual.
¿Es el componente accesible mediante navegación por teclado?
Los elementos de formulario nativos (input, button, a, label) gestionan el orden de tabulación y el foco automáticamente. La casilla 'recordarme' usa un input nativo type='checkbox', por lo que recibe el foco y responde a Espacio sin ARIA adicional. El botón de envío es type='submit', así que pulsar Enter en cualquier campo envía el formulario.