How to Make a Sliding Login Box with Remix, Tailwind, and Shadcn

How to Make a Sliding Login Box with Remix, Tailwind, and Shadcn

Hello fellow reader! Today we will be doing something really cool, We will be creating a sliding login/register screen with Tailwind, Shadcn UI and Remix!

If you want to skip the article and see the repo with the code you can find it here:

https://github.com/AlemTuzlak/remix-login

Setup

If you have shadcn-ui and tailwind setup in your Remix app skip to the next section.

To start us off I will assume you have tailwind already set up in your Remix application, if you don't follow these instructions to set it up and then come back to the article:

https://tailwindcss.com/docs/guides/remix

After that is done I will initialize Shadcn-ui, You can do that by following this link tutorial:

https://ui.shadcn.com/docs/installation/remix

Here is my components.json for reference:

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "default",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/tailwind.css",
    "baseColor": "slate",
    "cssVariables": true
  },
  "aliases": {
    "components": "~/components",
    "utils": "~/lib/utils"
  }
}

I will add the button and input components via the CLI by running:

npx shadcn-ui@latest add button input

If you're using something custom or already have this setup you can skip this part, You can use your components or whatever you like, the concept is important, not the input or the button components and the specific implementations!

Root modifications

So the root modifications are very minor, I just added some classes to help me out:

import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction } from "@remix-run/node";
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";
import stylesheet from "~/tailwind.css";

export const links: LinksFunction = () => [
  ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
  { rel: "stylesheet", href: stylesheet },
];

export default function App() {
  return (
-    <html lang="en">
+    <html  className="h-full overflow-x-hidden">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <Meta />
        <Links />
      </head>
-      <body>
+      <body className="overflow-y-auto">
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

I just hide overflow on the X axis, add a height of 100% on the html element and allow for scrolling on the Y axis on the body.

Layout

The first part of this whole trick is writing up the layout for all the routes! Let's do that now, First, add the following route to your routes folder:

_login.tsx

After that let's first add a loader and the following helper method:

const getOptionalUser = async (request: Request) => {
  // your implementation here
  return null;
};

export const loader = async ({ request }: LoaderArgs) => {
  // We get the optional user here
  const user = await getOptionalUser(request);
  // If the user exists on any of these routes we redirect the user to the dashboard
  // or wherever you need to redirect him
  if (user) {
    return redirect("/dashboard");
  }
  // Otherwise let the user login/register by returning null
  return null;
};

So this loader needs to check if the user is logged in and redirect accordingly if he is, You can use whatever you want to implement this! This is out of the scope of this article as we are focusing on the client part, not the server part.

Alright, after that is done, we will need a small helper to detect which URL we are exactly on. So we will need a helper to detect this, It's pretty straightforward:

const useGetCurrentPage = () => {
  // Used to retrieve the url
  const location = useLocation();
  // Gets the current path name (url)
  const url = location.pathname; 
  return {
    // Ends with login? We are on the login page
    isLoginPage: url.endsWith("/login"),
    // Ends with register? We are on the register page
    isRegisterPage: url.endsWith("/register"),
    // Ends with forgot password? we are on the forgot password page
    isForgotPasswordPage: url.endsWith("/forgot-password"),
  }
};

Layout

I will be showing you how to implement 3 screens:

  • login

  • register

  • forgot password

To get all of these screens running the most important part is implementing the actual layout that will be consumed by all of these routes! Let's start by adding the base layout and we will add pieces step by step and explain what we are doing:

export default function LoginLayout() { 
  const navigate = useNavigate();
  // We consume the helper we wrote above
  const { isForgotPasswordPage, isLoginPage, isRegisterPage } =
  useGetCurrentPage();
  // This will be used to show either login or register to the user.
  const key = isLoginPage ? "Login" : "Register";

  return (
    // We define a box element with width of 100% and full screen height and we align
    // everything to the middle of that box
    // We will also be adding shapes so we add the z-index and relative
    // and we hide the overflow
    <div className="relative z-10 flex min-h-screen w-full items-start justify-center overflow-hidden md:items-center">
      <!-- The code goes here -->
    </div>
  )
}

Alright, so we have added a full-screen box that will contain our flipping card and boxes, so let's now add the two boxes that will move from left to right depending on the screen the user is on:

<div className="relative z-10 flex min-h-screen w-full items-start justify-center overflow-hidden md:items-center">
  <div
    className={cn(
      // The color of the box, change to whatever you like!
      "bg-primary",
      // Change these values to whatever you like if you want to place it somewhere else
      "absolute top-1/2 -z-10 h-1/2 w-1/4 -translate-y-1/2 drop-shadow transition",
      // Places this box on the right side when it's login or forgot password page
      (isLoginPage || isForgotPasswordPage) &&
        "right-1/2 -translate-x-[45%]",
      // Otherwise transfers it to the left
      isRegisterPage && "left-1/2 translate-x-[45%]"
    )}
  />
  <div
    className={cn(
      // The color of the box, change to whatever you like!
      "bg-gradient-to-br from-indigo-500 from-10% via-sky-500 via-30% to-emerald-500 to-90%",
      // Placement of the box, feel free to tinker with it!
      "absolute top-1/2 -z-10 h-1/2 w-1/4 -translate-y-1/2 drop-shadow transition",
      // The opposite of the above box, on the left side on the two pages
      (isLoginPage || isForgotPasswordPage) && "left-1/2 translate-x-[45%]",
      // Moves to the right on navigation to register page
      isRegisterPage && "right-1/2  -translate-x-[45%]"
    )}
  />
</div>

Alright so these two boxes are purely to make the login look cooler, You can completely skip this part as it doesn't impact the final login screen. Now let's add the actual card that is going to slide between the screens:

export default function LoginLayout() {
  const { isForgotPasswordPage, isLoginPage, isRegisterPage } =
    useGetCurrentPage();

  const key = isLoginPage ? "Login" : "Register";
  return (
    <div className="relative z-10 flex min-h-screen w-full items-start justify-center overflow-hidden md:items-center">
    <!-- boxes from above -->
    <div className="relative z-10 flex h-screen w-full flex-col-reverse bg-white drop-shadow-2xl md:h-[75vh] md:w-11/12 md:flex-row lg:w-2/3">
      <!-- Left side box -->
      <div
        className={cn(
          // Color of the box, add what you want!
          "bg-gradient-to-br from-indigo-500 from-10% via-sky-500 via-30% to-emerald-500 to-90%",
          "z-20 flex h-full w-full origin-left scale-x-100 flex-col items-center justify-center p-4 px-8 transition-all md:w-1/2 lg:px-20",
          // On register page this box will be on the right side
          isRegisterPage && "md:translate-x-full",
          // On forgot password page this block will be hidden
          isForgotPasswordPage && " scale-x-0"
        )}
      > 
        <div className="items-center flex flex-col gap-4">
          <!-- Title & description but you can add whatever you want there -->
          <h1 className="text-center !text-6xl text-black">{key} Title</h1>
          <p className="font-semibold text-black">{key} Description</p>
          <!-- Used to switch between the two screens -->
          <Link to={isLoginPage ? "/register" : "/login"}>
            <Button>{key === "Login" ? "Register" : "Login"}</Button>
          </Link>
        </div>
      </div>
      <!-- Right side box -->
      <div
        className={clsx(
          "z-10 w-full p-8 transition-transform md:w-1/2 lg:p-0",
          isRegisterPage && "md:-translate-x-full",
          isForgotPasswordPage && "-translate-x-1/2"
        )}
      >
        <!-- Renders the current screen page -->
        <Outlet />
      </div>
    </div>
  </div>
  );
}

Alright, so what are we doing here? Well, we have two boxes, The first one contains additional information that you want to add to your login screen, and the other box contains the actual login/register form using the Outlet.

Using the information if we are on the login/register/forgot-password URL we shuffle the boxes from left to right and if on forgot password we hide one of the boxes to make the form full screen and we add transition transforms to make it look smooth!

And that is it for the layout! Now let's add the routes!

Routes

This part is completely up to you and your needs! I will be just giving you some basic code so you can see and tinker with the final result but you can add whatever you need here, login providers, additional inputs, form validation, form handling etc.

Let's add the new routes:

_login.login.tsx
_login.register.tsx
_login.forgot-password.tsx

And now let's start with the login screen, paste in the following code:

import { Form, Link } from "@remix-run/react";
import type { ActionArgs } from "@remix-run/node";
import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";

export const action = async ({}: ActionArgs) => {
  // your implementation here
  return null;
};

export default function LoginRoute() {
  return (
    <Form className="flex h-full items-center justify-center" method="post">
      <div className="flex flex-col gap-4 mx-auto h-full w-full items-center justify-center lg:w-2/3">
        <h1 className="mb-2 text-center text-6xl text-black lg:mb-4">Login</h1>
        <p className="text-center">Login below</p>
        <Input
          placeholder="Enter your username/email"
          autoFocus
          className="w-full"
          name="identifier"
        />
        <Input
          placeholder="Enter your password"
          className="w-full"
          name="password"
          type="password"
        />
        <Link to={"/forgot-password"}>Forgot password?</Link>
        <Button type="submit" size="lg">
          Login
        </Button>
      </div>
    </Form>
  );
}

Register page:

import type { ActionArgs } from "@remix-run/node";
import { Form } from "@remix-run/react";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input"; 

export const action = async ({ }: ActionArgs) => {
  // your implementation here
  return null;
};

export default function RegisterRoute() {
  return (
    <Form
      className="mx-auto flex h-full w-full items-center justify-center"
      method="post"
    >
      <div
        className="flex flex-col gap-4 mx-auto h-full w-full items-center justify-center lg:w-2/3" 
      >
        <h1 className="mb-2 text-center text-6xl text-black lg:mb-4">
          Register
        </h1>
        <p className="text-center">Register below</p>
        <Input
          autoFocus
          className="w-full"
          placeholder="Enter your username"
          name="username"
        />
        <Input
          name="email"
          placeholder="Enter your email"
          type="email"
          required
          className="w-full"
        />
        <Input placeholder="Enter your password" name="password" type="password" required className="w-full" />
        <Input placeholder="Confirm password" type="password" className="w-full" name="confirmPassword" />

        <Button type="submit" size="lg">
          Register
        </Button>
      </div>
    </Form>
  );
}

Forgot password page:

import { Link } from "@remix-run/react";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";

export default function ForgotPasswordRoute() {
  return (
    <div className="flex flex-col gap-4 mx-auto h-full w-full items-center justify-center lg:w-2/3">
      <h1 className="mb-2 text-6xl text-black text-center lg:mb-4">
        Forgot password
      </h1>
      <p className="text-center">Forgot password description</p>
      <Input className="w-full" placeholder="Enter your email" name="email" />
      <Link to="/login"></Link>
      <Button size="lg">Send password reset email</Button>
    </div>
  );
}

And that is pretty much it! You have a fully working switch box between the screens!

Final product

Here are our pages in action:

login page

Thank you!

If you've reached the end you're a champ! I hope you liked my article.

If you wish to support me follow me on Twitter here:

twitter.com/AlemTuzlak59192

or if you want to follow my work you can do so on GitHub:

github.com/AlemTuzlak

And you can also sign up for the newsletter to get notified whenever I publish something new!