How to create a custom sign-in/sign-up form

By Vlad Ionash — Published

If you’re familiar with Kinde, you’ve likely seen the default sign-in template that appears upon clicking the sign-in button. While this template is customizable, it doesn’t offer the full flexibility that many developers crave.

That’s about to change, as we now have the capability to build completely custom pages. In this guide, we’ll walk you through how to create a branded sign-in page that functions seamlessly with Kinde’s authentication flow!

Video version of this guide for reference

What you will need:

Link to this section

Before we get started:

Link to this section

Test your setup to ensure everything is working as intended. Start your server and navigate to your application to see the starter kit in action. Make sure you are able to Sign-in and Register.

  1. Navigate to SettingsAuthentication
  2. Press Configure on the authentication providers you want to use. We will be utilizing email and Google SSO for this tutorial.
  3. Record the Connection IDs for the email configuration and make sure to turn on the type of authorization you want your app to use on the bottom.
  1. Do the same for the Google authentication, making sure that it is on for your application at the bottom as well.
  1. Navigate to ApplicationsView Details on the application that you are using.
  2. At the bottom of the page, make sure you have the Use your own sign-up and sign-in screens setting turned on.

Navigate to your Starter Kit’s .env file. Append the following to it:

NEXT_PUBLIC_KINDE_CONNECTION_EMAIL_PASSWORDLESS=conn_fa48b6ab34cf4139b8186b4ef86c8cbb
NEXT_PUBLIC_KINDE_CONNECTION_EMAIL_PASSWORD=conn_d862295b24894ce6bde802ed74e8d6c7
NEXT_PUBLIC_KINDE_CONNECTION_GOOGLE=conn_bd498b8765b54a09a1ca29bda7a83d4d

Fill each variable with your own personal details of the Connection IDs that you recorded from the previous step.

Styling the Authentication Page:

Link to this section

This is the fun part—designing your custom authentication page. For this project we will be using some custom CSS files and setting up new containers and styles to get the look and feel you desire. Below is a sample I’ve created that somewhat matches Google’s Sign In flow with some liberal use of color, inspired by the 80s. Going over CSS and the attachments would take up too much time, so feel free to copy this over for now into your page.module.css and append it to the bottom of the file.

.container {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background: #091833; /* Dark background for contrast */
    background-image: linear-gradient(315deg, #091833 0%, #42275a 74%); /* Gradient background */
    font-family: "Poppins", sans-serif; /* Modern font, make sure to include it in your <head> */
}

.card {
    width: 350px;
    padding: 2rem;
    background-color: rgba(255, 255, 255, 0.05); /* Slight transparency */
    backdrop-filter: blur(5px); /* Blur effect for background */
    border-radius: 8px;
    border: 1px solid rgba(255, 255, 255, 0.2); /* Light border */
    color: #ffffff; /* White text color */
    text-align: center;
}

.logo img {
    width: 75px;
    height: auto;
    margin-bottom: 1rem;
}

.title {
    margin-bottom: 2rem;
    font-size: 24px;
    font-weight: bold;
    color: #ff00ff; /* Neon pink title */
    text-transform: uppercase;
    letter-spacing: 2px;
}

.form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.input {
    padding: 10px;
    background: rgba(255, 255, 255, 0.1);
    border: 1px solid rgba(255, 255, 255, 0.4);
    border-radius: 4px;
    color: #ffffff;
    font-size: 16px;
}

.button {
    padding: 15px 30px;
    background-color: #ff007f; /* Neon pink button */
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 16px;
    cursor: pointer;
    transition: background-color 0.3s ease;
}

.button:hover {
    background-color: #ff00ff; /* Brighter neon pink on hover */
}

.footer {
    margin-top: 2rem;
    font-size: 14px;
}

.link {
    color: #00ffff; /* Neon blue link */
    text-decoration: none;
}

.link:hover {
    text-decoration: underline;
}

.authMethods {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
}

.googleButton {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 12px 16px;
    background-color: #4285f4; /* Google's blue color */
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 16px;
    cursor: pointer;
    transition: background-color 0.3s ease;
    width: 100%; /* Match the width of the email input */
    box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.16); /* Add a subtle shadow for depth */
}

.googleButton:hover {
    background-color: #357ae8; /* Slightly darker blue on hover */
}

.googleIcon {
    margin-right: 10px; /* Space between the icon and the text */
    font-size: 20px; /* Icon size */
}

.input {
    width: 100%; /* Ensure the input matches the button width */
}

.overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 1000;
}

.closeButton {
    position: absolute;
    top: 10px;
    right: 10px;
    padding: 5px 10px;
    cursor: pointer;
}

Creating Routes and Components:

Link to this section

Create a new route for your authentication page src/app/sign-in and paste the following into it’s corresponding page.tsx file.

// sign-in/AuthPage.tsx
import React from "react";
import styles from "../page.module.css";
import {RegisterLink, LoginLink} from "@kinde-oss/kinde-auth-nextjs/components";
import EmailInput from "../components/Email"; // Adjust the path as needed
import {FaGoogle} from "react-icons/fa"; // Make sure to install react-icons if not already

const AuthPage: React.FC = () => {
    return (
        <div className={styles.container}>
            <div className={styles.card}>
                <div className={styles.logo}>
                    {/* Update the logo to use Google's logo or your own brand logo */}
                    <img
                        loading="lazy"
                        decoding="async"
                        fetchpriority="low"
                        src="../favicon.ico"
                        alt="Logo"
                    />
                </div>
                <h1 className={styles.title}>Custom Sign In</h1>
                <div className={styles.authMethods}>
                    <LoginLink
                        className={styles.googleButton}
                        authUrlParams={{
                            connection_id: process.env.NEXT_PUBLIC_KINDE_CONNECTION_GOOGLE || ""
                        }}
                    >
                        <FaGoogle className={styles.googleIcon} />
                        Sign in with Google
                    </LoginLink>
                    {/* Use the client component for email input */}
                    <EmailInput />
                </div>
                <div className={styles.footer}>
                    <span>
                        Don&apos;t have an account?{" "}
                        <RegisterLink className="btn btn-dark">Create account</RegisterLink>
                    </span>
                </div>
            </div>
        </div>
    );
};

export default AuthPage;

For testing, we can comment out the <EmailInput /> by replacing it with {/* <EmailInput /> */} for now, and navigate to the page by appending the route. If you’re using the base localhost, it should be navigable by going to: http://localhost:3000/sign-in. You should see something like this with a fully functional Google Sign-In button!

The page is just a standard form page, but let’s take a look at the authentication portion:

<LoginLink
    className={styles.googleButton}
    authUrlParams={{
        connection_id: process.env.NEXT_PUBLIC_KINDE_CONNECTION_GOOGLE || ""
    }}
/>

All we have to do to attach the Google Sign-in is pass the connection_id of the Google SSO identity connection from our Kinde page to the authUrlParams parameter. The rest is up to you on how you will want the button styled. Incredibly simple!

We can even do the same thing for the “Create account” button.

<RegisterLink className="btn btn-dark">Create account</RegisterLink>

If we would want the user to register through a connection_id of our choice, just simply add the authUrlParams section like so:

<RegisterLink
    className="btn btn-dark"
    authUrlParams={{
        connection_id: process.env.NEXT_PUBLIC_KINDE_CONNECTION_GOOGLE || ""
    }}
>
    Create account
</RegisterLink>

Creating a New Component for Email Input

Link to this section

Now let’s talk about the creation of a new component dedicated to handling email input. This component is a pivotal part of our authentication flow, as it allows users to enter their email address as a precursor to the authentication process.

As before, the actual authentication portion is quite simple; we will simply be adding a login_hint parameter to a new <LoginLink> component like so:

<LoginLink
    authUrlParams={{
        connection_id: process.env.NEXT_PUBLIC_KINDE_CONNECTION_EMAIL_PASSWORDLESS || "",
        login_hint: email
    }}
/>

Here we simply are passing the connection ID that is being stored in our .env file to the connection type that we are using (in this case, being ‘passwordless’) like we did for the Google SSO connection and the login_hint which pertains to the user’s email.

This is all that we need to route the email away from Kinde’s proprietary screen to use your own custom-designed authentication solution. This streamlines the authentication process by pre-filling the email field when the user is redirected to the authentication service and bypasses that initial input you would normally see with a standard Kinde auth implementation. This works with the <RegisterLink> component in the exact same way.

You could even attach the input form in your header of the base app, skipping another page entirely! Or even have it set up as a pop-up modal in an iframe, so the user isn’t even redirected.

💡 Remember: everything that is passed above, including the connection_id, is not sensitive data. Even if the user is somehow redirected or hijacked, Kinde’s auth screen will circumvent an imposter from logging in and stealing cookies. This is a big benefit to Kinde’s implementation of custom sign-ins

To construct this component, create a new directory src/app/components and create a new file named email.tsx within that folder with the following content:

// components/EmailInput.tsx
"use client";
import React, {useState} from "react";
import styles from "../page.module.css";
import {LoginLink} from "@kinde-oss/kinde-auth-nextjs/components";

export default function EmailInput() {
    const [email, setEmail] = useState("");

    const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        setEmail(event.target.value);
    };

    return (
        <div className={styles.inputWrapper}>
            <input
                type="email"
                placeholder="Email"
                required
                value={email}
                onChange={handleEmailChange}
                className={styles.input}
            />
            <LoginLink
                authUrlParams={{
                    connection_id:
                        process.env.NEXT_PUBLIC_KINDE_CONNECTION_EMAIL_PASSWORDLESS || "",
                    login_hint: email
                }}
            >
                <button type="button" className={styles.button}>
                    NEXT
                </button>
            </LoginLink>
        </div>
    );
}

This email.tsx component will serve as the vessel through which users will input their email addresses. It by itself will look like this (assuming you did not change the CSS styling from my template):

Breaking the code down, it is essentially a standard input form that will handle said inputs and save them to state to be able to pass to Kinde.

Navigate back to your src/app/sign-in/page.tsx file and uncomment <EmailInput /> if you haven’t already. Navigating back to http://localhost:3000/sign-in, you should now have something that looks like this:

Finalizing the Sign-In Page:

Link to this section

We are almost done! Now all that is left to do is to make this page navigable from your website’s perspective. Let’s add a link to the Sign-in page in the header. To do so, navigate to srs/app/layout.tsx and add the following adjustments that are wrapped in the comment sections (the rest of the code should be the same and is being used as a reference):

{
    !(await isAuthenticated()) ? (
        <>
            <LoginLink className="btn btn-ghost sign-in-btn">Sign in</LoginLink>

            {/* This is the newly added link for our new sign-in page */}
            <Link className="btn btn-ghost sign-in-btn" href="/sign-in">
                Sign in custom
            </Link>
            {/* This is the newly added link for our new sign-in page */}

            <RegisterLink className="btn btn-dark">Sign up</RegisterLink>
        </>
    ) : (
        <div className="profile-blob">
            {user?.picture ? (
                <img
                    loading="lazy"
                    decoding="async"
                    fetchpriority="low"
                    className="avatar"
                    src={user?.picture}
                    alt="user profile avatar"
                    referrerPolicy="no-referrer"
                />
            ) : (
                <div className="avatar">
                    {user?.given_name?.[0]}
                    {user?.family_name?.[0]}
                </div>
            )}
            <div>
                <p className="text-heading-2">
                    {user?.given_name} {user?.family_name}
                </p>

                <LogoutLink className="text-subtle">Log out</LogoutLink>
            </div>
        </div>
    );
}

This will add a new navigable button on the top-right of our starter-kit example, which should look like this:

Now, you might notice that the Sign-in page is technically inside the header and footer and might not look as professional as you want it.

To do so, simply utilize NextJS’s router layout configuration and adjust it as you want. An easy implementation would be to create two nested folders src/app/(sign_in) and src/app/(dashboard).

Move the contents of the original layout.tsx and dashboard folder inside the newly created src/app/(dashboard).

Move your sign-in folder inside the newly created (sign_in) folder, and add a layout.tsx file with the minimal configurations. An example layout.tsx would be:

// app/layout.tsx
import {ReactNode} from "react";

type Props = {
    children: ReactNode;
};

const MinimalistLayout = ({children}: Props) => {
    return <>{children}</>;
};

export default MinimalistLayout;

This will then configure the layouts appropriately and not have the sign-in page show the headers and footers. Voila, you should know have a fully functional custom sign-in page!

With these steps, you’ve now created a custom sign-in page that integrates perfectly with Kinde’s authentication system. The process involves a fair amount of code configuration, but the end result is a fully branded and functional sign-in experience for your users.

Remember, the GitHub repository will be publicly available, allowing you to dive deeper into the changes made throughout this guide. If you encounter any challenges or have questions, don’t hesitate to reach out to us on Slack, Discord, or Intercom for support.

We hope this guide has been informative and helpful in your journey to create a custom sign-in page that stands out. Happy coding, and here’s to building an amazing product with Kinde’s authentication!

Link to the repo example: https://github.com/VeryKinde/kinde-nextjs-app-router-starter-kit-custom-sign-in-example