How to build a split-screen login page in React
A split-screen login page in React uses CSS Grid with two equal columns: a left panel for brand identity and a right panel for the form. The form panel fades and slides in with a Framer Motion spring on mount, keeping the entry smooth without a full-page transition.
- Stack: React + Framer Motion + Tailwind v4, ~130 lines, zero extra dependencies.
- Layout: lg:grid-cols-2, left panel hidden on mobile, form panel full-width below lg breakpoint.
- Entry animation: opacity 0→1 + y 20→0 over 600ms with a custom cubic-bezier spring (0.16, 1, 0.3, 1).
- Props: showSocial and showRemember toggle optional UI blocks without conditional CSS hacks.
- Theming via CSS custom properties only, no hardcoded colors, works across all presets.
Auth Login Split is a full-screen two-column login page built with CSS Grid. The left column is a branding canvas, accent background, product name, tagline, and the right column holds the actual form. On mobile the left panel collapses entirely, leaving a clean, centered login form. The form enters with a subtle Framer Motion fade-and-slide, which is enough movement to feel polished without distracting users who just want to sign in.
Anatomy
The outer section is a min-h-screen grid with lg:grid-cols-2. The left panel (hidden below lg) is a flex column using justify-between to push the logo to the top, the tagline to the middle, and the copyright to the bottom; it uses var(--color-accent) as background. The right panel centers a max-w-sm div that contains the heading, optional Google button, a divider, the email and password inputs, an optional remember/forgot row, the submit button, and a sign-up link.
How it works
The animation is minimal but precise. The max-w-sm form wrapper is a motion.div with `initial={{ opacity: 0, y: 20 }}` and `animate={{ opacity: 1, y: 0 }}`. The transition uses a named cubic-bezier ease `[0.16, 1, 0.3, 1]`, an aggressive ease-out curve that starts fast and settles softly, giving the form a natural drop-in feeling. Duration is 0.6s. Because the animation targets the wrapper rather than individual fields, the whole form enters as a unit, which reads cleaner than staggered field animations in an auth context.
How to build it in React
Set up the two-column grid
Make the outer section a full-viewport grid with two equal columns above the lg breakpoint. Below lg, CSS Grid falls back to a single stacked column automatically, so the form takes the full width on mobile without any extra media queries.
<section className="min-h-screen grid lg:grid-cols-2"> {/* left panel */} <div className="hidden lg:flex flex-col justify-between p-12" style={{ background: "var(--color-accent)" }}> ... </div> {/* right panel */} <div className="flex items-center justify-center p-8"> ... </div> </section>Animate the form wrapper on mount
Wrap the form content in a motion.div and declare the initial and animate states. The cubic-bezier `[0.16, 1, 0.3, 1]` is a fast ease-out that gives the animation energy without a bounce. Animate the wrapper, not individual inputs, so the form enters as a single cohesive block.
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" >Toggle optional blocks with props
Gate the social login block behind a showSocial boolean and the remember/forgot row behind showRemember. Both default to true. This keeps the component useful across contexts, a minimal internal tool drops social login, a consumer SaaS keeps everything, without touching the markup.
{showSocial && ( <> <button ...>Continuer avec Google</button> <div className="flex items-center gap-3 my-6">...</div> </> )}Apply CSS tokens for theming
Every color references a CSS custom property: var(--color-accent) for the left panel background and submit button, var(--color-foreground) for text, var(--color-border) for input borders and dividers. This makes the component switch themes automatically when the data-theme attribute changes on a parent element.
<input type="email" style={{ background: "var(--color-background-alt)", color: "var(--color-foreground)", border: "1px solid var(--color-border)", }} />
When to use it
The split layout works well for SaaS products and agencies where the login screen is a brand touchpoint. The left panel is prime space to reinforce the value proposition right before users sign in. Skip it for internal tools or dashboards where nobody cares about branding at login time, a simpler centered form is faster to scan. The layout also requires enough content for both columns to feel balanced; a left panel with just a logo and an empty tagline looks worse than a single-column design.
Used by
- Vercel, Two-column auth layout with brand messaging on the left and the login form on the right.
- Linear, Split login page with brand identity panel and email/social login form, consistent with their clean product aesthetic.
- Notion, Two-column auth screen that keeps product branding visible throughout the login flow.
FAQ
What happens to the left panel on mobile?
It disappears entirely. The div carries hidden lg:flex, so below the lg breakpoint (1024px) it is display:none and the form panel expands to full width. No layout shift, no collapsed branding element taking up space.
How do I connect the form to a real auth provider?
Replace the onSubmit={(e) => e.preventDefault()} handler with your auth logic, NextAuth signIn(), Supabase auth.signInWithPassword(), or a custom API call. The form is intentionally uncontrolled in the demo, so you can add your own state management without fighting existing controlled inputs.
Can I use a background image or illustration on the left panel?
Yes. Replace the background style from var(--color-accent) to a CSS background-image or add an absolute-positioned img/Image inside the left div. Keep the text readable by adding a semi-transparent overlay if the image is busy.
Is the component accessible?
The form uses native input elements with type='email' and type='password', which gives browsers built-in validation and assistive technology support. Labels are missing from the demo inputs, add explicit label elements or aria-label attributes before shipping to production.