How to build a registration form in React with a fade-in animation
A React registration form typically centers a card with brand logo, optional social OAuth button, a first/last name grid row, email, password, password confirmation, and a terms checkbox. Wrap the card in a Framer Motion div with `initial={{ opacity: 0, y: 20 }}` and `animate={{ opacity: 1, y: 0 }}` for a polished entrance that costs one component and no extra library.
- Stack: React 18 + Framer Motion 11 + Tailwind v4, ~70 lines, zero extra dependencies.
- Entrance animation: single motion.div, cubic-bezier [0.16, 1, 0.3, 1], duration 0.6s.
- Social login row is conditional via `showSocial` prop, toggle it off for email-only flows.
- All colors come from CSS custom properties (--color-accent, --color-background, etc.), drop-in theming across the 7 presets.
- Accessible: native <label> + checkbox, semantic form element, password fields typed correctly for browser autocomplete.
AuthRegister is a minimal, centered sign-up card built for SaaS and universal products. It handles the full registration surface, brand mark, social OAuth shortcut, split first/last name row, email, password, confirmation, and terms acceptance, in a single composable component. The whole card slides in on mount via a single Framer Motion animation, keeping the interaction light without a complex animation setup.
Anatomy
The component is structured as three vertical zones inside a `max-w-sm` centered column. At the top: a brand monogram square (first letter of `brandName`), an H1 title, and an optional subtitle. In the middle: a conditional Google OAuth button followed by a divider, then the form itself, two inputs side-by-side in a 2-column grid (first name / last name), followed by stacked email, password, and confirm-password inputs, a terms checkbox, and a full-width submit button. At the bottom: a login redirect link. The outer section spans `min-h-screen` so the card stays vertically centered on all screen heights.
How it works
The animation is intentionally simple: one `motion.div` wraps the entire card with `initial={{ opacity: 0, y: 20 }}` and `animate={{ opacity: 1, y: 0 }}`. The custom cubic-bezier `[0.16, 1, 0.3, 1]` is an expo-out curve, it accelerates quickly and decelerates smoothly, making the card feel like it settles into place rather than just fading in. Duration is 0.6s, which is long enough to feel deliberate but short enough not to block the user. No scroll trigger, no stagger, just a clean page-load entrance that works everywhere without extra setup.
How to build it in React
Set up the centered layout
Wrap everything in a `section` with `min-h-screen flex items-center justify-center`. Set the background to `var(--color-background)` via an inline style so it respects the active theme preset. Inside, place a `motion.div` constrained to `max-w-sm w-full`, this is the card boundary.
<section className="min-h-screen flex items-center justify-center py-16 px-6" style={{ background: "var(--color-background)" }} > <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }} className="w-full max-w-sm" >Add the brand header and optional social button
Render a small square using the first character of `brandName` as the monogram, styled with `--color-accent` background. Below it, place the H1 title and optional subtitle. Then, conditionally render the Google button and a divider based on the `showSocial` prop. The divider uses a flex row with two `h-px` lines and a centered 'ou' / 'or' label.
{showSocial && ( <> <button style={{ border: "1px solid var(--color-border)", color: "var(--color-foreground)" }} className="w-full py-2.5 rounded-lg text-sm font-medium mb-3 transition-opacity hover:opacity-80"> Sign up with Google </button> <div className="flex items-center gap-3 my-6"> <div className="flex-1 h-px" style={{ background: "var(--color-border)" }} /> <span className="text-xs" style={{ color: "var(--color-foreground-light)" }}>or</span> <div className="flex-1 h-px" style={{ background: "var(--color-border)" }} /> </div> </> )}Build the form fields
Inside a `form` with `onSubmit={(e) => e.preventDefault()}`, open with a `grid grid-cols-2 gap-3` div containing the first and last name inputs. Stack email, password, and confirm-password below. All inputs share the same styling: `background: var(--color-background-alt)`, `border: 1px solid var(--color-border)`, and `color: var(--color-foreground)`. This keeps them theme-aware without hardcoding any hex.
<div className="grid grid-cols-2 gap-3"> <input type="text" placeholder="First name" style={{ background: "var(--color-background-alt)", color: "var(--color-foreground)", border: "1px solid var(--color-border)" }} className="w-full px-4 py-3 rounded-lg text-sm outline-none" /> <input type="text" placeholder="Last name" ... /> </div> <input type="email" placeholder="Email" ... /> <input type="password" placeholder="Password" ... /> <input type="password" placeholder="Confirm password" ... />Add the terms checkbox and submit button
Wrap a native `<input type='checkbox'>` in a `<label>` with `flex items-start gap-2`. This keeps the checkbox and the terms text accessible without a custom component. The submit button spans full width with `background: var(--color-accent)` and `color: var(--color-background)`. Close the form with a paragraph linking to the login page via the `loginUrl` prop.
When to use it
This component fits any product that needs a fast, self-contained registration screen: SaaS dashboards, internal tools, side projects, or admin portals. The `showSocial` prop makes it easy to toggle between OAuth-first and email-only flows without forking the component. Avoid it when your registration flow requires multi-step onboarding (progress bar, role selection, plan picker), those flows need a stateful wizard, not a single-card form. Also skip it when you need real form validation with error states per field; add a library like react-hook-form before shipping.
Used by
- Linear, Centered sign-up card with Google OAuth button and minimal field set, matching this exact pattern.
- Vercel, Registration flow with social-first (GitHub/Google) options above a divider, then email fallback below, the same conditional social block this component uses.
- Supabase, Auth UI library built on the same centered card, split name fields, and password confirmation pattern.
- Clerk, Pre-built sign-up component with brand logo slot, social OAuth row, and stacked email/password fields, the reference implementation for this card pattern in the React ecosystem.
FAQ
How do I add real form validation to this component?
Replace the bare `<input>` elements with react-hook-form's `register` and surface per-field error messages below each input. The layout stays the same; you're just adding a `useForm` hook and conditional error `<span>` tags. For email format and password strength, use the built-in pattern option or a Zod schema with the zodResolver adapter.
Can I switch to a two-column layout for wider screens?
The card is constrained to `max-w-sm` by design. For a split layout, illustration on the left, form on the right, see the auth-login-split variant, which this component pairs with. Stretching a single-column form beyond ~380px hurts usability; the wider space is better used for a visual or social-proof panel.
What happens to the animation if the user has reduced-motion enabled?
Framer Motion respects the `prefers-reduced-motion` media query by default when you use the `useReducedMotion` hook and set your transitions conditionally. In the current implementation you should add `const shouldReduce = useReducedMotion()` and replace the transition with `duration: shouldReduce ? 0 : 0.6` to silence the animation for users who have opted out.
Is the terms checkbox wired to disable the submit button?
Not in this base component, it renders the checkbox as uncontrolled. To enforce acceptance before submission, lift the checkbox into local state with `useState(false)` and add `disabled={!accepted}` plus a reduced-opacity style to the submit button. Keep the disabled state visually distinct so users understand why the button is inactive.