Auth.js Google sign-in page for Next.js

An animated sign-in card for Auth.js v5: the Google and GitHub buttons call signIn, Auth.js sends people back to your own /login page, and its error codes show up as plain sentences.

Gateway Dive, preview
One payment$3

By buying you agree to the Terms.

Ready forCursorv0Lovableshadcn CLI

Full product pageReact · Tailwind · TypeScript

Use it in Next.js

app/login/login-card.tsx

"use client";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import GatewayDive from "@/components/gateway-dive/GatewayDive";

const providers = [
  { id: "google", label: "Google" },
  { id: "github", label: "GitHub" },
];

export default function LoginCard({ callbackUrl, error }: { callbackUrl: string; error?: string }) {
  const router = useRouter();

  return (
    <main style={{ height: "100dvh", position: "relative" }}>
      {error ? (
        <p role="alert" className="absolute inset-x-0 top-4 z-10 text-center text-sm text-red-400">{error}</p>
      ) : null}
      <GatewayDive
        autoplayDemo={false}
        brand="Acme"
        providers={providers}
        onProvider={(id) => void signIn(id, { redirectTo: callbackUrl })}
        onSubmit={async ({ email, password }) => {
          const res = await signIn("credentials", { email, password, redirect: false });
          return res?.error ? { ok: false, error: "Wrong email or password." } : { ok: true };
        }}
        onSuccess={() => setTimeout(() => router.push(callbackUrl), 1400)}
      />
    </main>
  );
}

app/login/page.tsx

import LoginCard from "./login-card";

const messages: Record<string, string> = {
  OAuthAccountNotLinked: "That email already signed in another way. Use the same button as last time.",
  AccessDenied: "Access was denied. Try another account.",
};

export default async function LoginPage({ searchParams }: { searchParams: Promise<{ callbackUrl?: string; error?: string }> }) {
  const { callbackUrl, error } = await searchParams;
  const safe = callbackUrl?.startsWith("/") && !callbackUrl.startsWith("//") ? callbackUrl : "/dashboard";
  return <LoginCard callbackUrl={safe} error={error ? messages[error] ?? "Sign-in didn't work. Try again." : undefined} />;
}

auth.ts

import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
import { checkPassword } from "@/lib/users"; // your own lookup: returns the user or null

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Google,
    GitHub,
    Credentials({
      credentials: { email: {}, password: {} },
      authorize: async (c) => checkPassword(String(c.email), String(c.password)),
    }),
  ],
  session: { strategy: "jwt" },
  pages: { signIn: "/login" }, // Auth.js sends people (and its errors) here instead of its default page
});

app/api/auth/[...nextauth]/route.ts

import { handlers } from "@/auth";

export const { GET, POST } = handlers;

Good to know

  • We type-checked these files against next-auth 5.0.0-beta.32 and next 15.5. In v5, signIn from next-auth/react takes redirectTo. callbackUrl still works but is marked deprecated.
  • Google and GitHub read AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET, AUTH_GITHUB_ID and AUTH_GITHUB_SECRET from the environment, plus AUTH_SECRET. In the Google Cloud console the redirect URI is http://localhost:3000/api/auth/callback/google, and the same path on your live domain.
  • pages.signIn points Auth.js at /login, so its own grey page never shows. When a sign-in fails, Auth.js comes back to /login with ?error= and a code. The server page turns the codes you care about into a sentence for the card.
  • The card always has an email and password form. Here it goes to a Credentials provider. If you only want the buttons, return { ok: false, error: "Use Google or GitHub" } from onSubmit, or follow our Auth.js credentials guide for the password side.
  • Copy GatewayDive.tsx, engine.ts and gateway-dive.css from react/src/components/gateway-dive. backdrop takes your own screen to sit behind the card; without it you get the bundled demo dashboard.

Install

  1. Copy from the zipPut react/src/components into src/, then npm i next-auth@beta.
  2. Or ask your AIPaste PROMPT.md into Cursor, Lovable or v0.

Props

providers
{ id, label }[]; the id is the Auth.js provider id
onProvider
(id) → signIn(id, { redirectTo })
onSubmit
email and password, returns { ok, error? }
onSuccess
runs when the check appears; route here
backdrop
your own screen behind the card
autoplayDemo
false in a real app

How qodesign compares

All comparisons, with prices read on each library's own site.

Questions

Why does Google send people to a different page after sign-in?

Auth.js only redirects to paths on your own site. Pass redirectTo with a relative path like /dashboard, and keep the callbackUrl it hands to /login, as the server page above does.

What does OAuthAccountNotLinked mean?

The email already belongs to an account that signed in with another provider. By default Auth.js won't link the two for you, so the page asks people to use the same button as last time.

Does it work with NextAuth v4?

The card is the same. In v4 you call signIn(id, { callbackUrl }) from next-auth/react and set pages.signIn in authOptions.

Does the sign-in page work in Next.js?

Yes, as a client component. Keep "use client" at the top of the file that renders it, as in the snippet above. App Router and Pages Router both work.

Can Cursor or v0 add it for me?

Yes. Each download has a PROMPT.md. Paste it into Cursor, Lovable, v0 or Claude Code and it fits the piece into your app.

Can I use it in client work?

Yes, in personal and client projects. You can't resell the code itself as a kit.