Customize Sign-In

Learn how to customize the sign-in page styling, validation, and behavior

Learn how to customize Plainform's sign-in page to match your brand and add custom functionality.

Goal

By the end of this recipe, you'll have customized the sign-in page with your own styling, validation rules, and behavior.

Prerequisites

  • A working Plainform installation
  • Basic knowledge of React and TypeScript
  • Familiarity with Tailwind CSS

Plainform uses custom sign-in forms built with Clerk's useSignIn hook, not Clerk's pre-built components. This gives you full control over the UI.

Steps

Locate the Sign-In Form

The sign-in form is located at:

components/user/SignInForm.tsx

This component uses:

  • useSignIn hook from Clerk for authentication logic
  • react-hook-form for form state management
  • Zod for validation
  • Tailwind CSS for styling

Customize Form Styling

Update the form's appearance by modifying Tailwind classes:

components/user/SignInForm.tsx
export function SignInForm({ isInModal = false }: ISignInFormProps) {
  // ... form logic

  return (
    <div className="w-full flex items-center justify-center h-full flex-col">
      <div className="w-full flex flex-col gap-8 max-w-96 justify-center flex-auto">
        <div className="gap-8 flex flex-col w-full">
          {/* Customize header */}
          <StepHeader
            title="Welcome Back"
            description="Sign in to your account to continue"
          />
          
          {/* OAuth buttons */}
          <div className="w-full flex flex-col gap-4">
            <OAuthConnection
              strategy="oauth_google"
              lastUsed={lastStrategy}
              icon="Google"
            >
              Continue with Google
            </OAuthConnection>
            <OAuthConnection
              lastUsed={lastStrategy}
              strategy="oauth_github"
              icon="GitHub"
            >
              Continue with GitHub
            </OAuthConnection>
          </div>

          {/* Separator */}
          <div className="flex gap-3 items-center text-neutral-foreground">
            <Separator className="shrink" />
            Or continue with email
            <Separator className="shrink" />
          </div>

          {/* Email/password form */}
          <form onSubmit={handleSubmit(onSubmit)}>
            {/* Form fields */}
          </form>
        </div>
      </div>
      <LegalReminder />
    </div>
  );
}

Common customizations:

  • Update StepHeader title and description
  • Modify OAuth button text
  • Change separator text
  • Adjust spacing with gap-* classes
  • Add brand colors to buttons and text

Customize Validation Rules

Update the validation schema in validationSchemas/authSchemas.ts:

validationSchemas/authSchemas.ts
import { z } from 'zod';

export const signInSchema = z.object({
  identifier: z
    .string()
    .min(1, 'Email is required')
    .email('Please enter a valid email address'),
  password: z
    .string()
    .min(8, 'Password must be at least 8 characters')
    .max(100, 'Password is too long'),
});

Validation options:

  • Change minimum password length
  • Add custom error messages
  • Add regex patterns for email format
  • Add maximum length constraints

Keep validation rules consistent between sign-in and sign-up forms for better UX.

Customize Sign-In Logic

The sign-in form uses Clerk's useSignIn hook:

components/user/SignInForm.tsx
import { isClerkAPIResponseError } from '@clerk/nextjs/errors';
import type { ClerkAPIError } from '@clerk/types';

const onSubmit: SubmitHandler<IFormData> = async (data) => {
  const { identifier, password } = data;

  if (!isLoaded) return;

  try {
    const signInAttempt = await signIn.create({
      identifier,
      password,
    });

    if (signInAttempt.status === 'complete') {
      await setActive({ session: signInAttempt.createdSessionId });
      router.push('/');
      window.location.reload();
    }
  } catch (err: unknown) {
    if (!isClerkAPIResponseError(err)) return;

    err.errors.forEach((error: ClerkAPIError) => {
      const paramName = error.meta?.paramName as keyof IFormData | undefined;
      if (paramName && error.longMessage) {
        setError(paramName, {
          type: 'manual',
          message: error.longMessage,
        });
      } else if (paramName === undefined) {
        toast.error(error?.message);
      }
    });
  }
};

Key points:

  • Use signIn.create() with the submitted identifier and password
  • Use setActive() with the returned createdSessionId
  • Redirect to / and reload so server-rendered session-dependent UI refreshes
  • Map Clerk API errors into React Hook Form field errors when possible

Preview Your Changes

Start your development server:

npm run dev

Navigate to http://localhost:3000/sign-in to see your customized sign-in page.

Common Customizations

Change Form Layout

Switch to a horizontal layout for wider screens:

components/user/SignInForm.tsx
<div className="grid md:grid-cols-2 gap-6">
  <div className="space-y-4">
    {/* Left side: Form */}
    <SignInForm />
  </div>
  <div className="hidden md:flex items-center justify-center bg-muted rounded-lg">
    {/* Right side: Image or branding */}
    <img src="/signin-illustration.svg" alt="Sign in" />
  </div>
</div>

Add More OAuth Providers

Plainform includes an OAuthConnection component for OAuth buttons. To add more providers:

components/user/SignInForm.tsx
import { OAuthConnection } from '@/components/user/OAuthConnection';
import { useClerk } from '@clerk/nextjs';

export function SignInForm() {
  const { client } = useClerk();
  const lastStrategy = client?.lastAuthenticationStrategy;

  return (
    <div className="w-full flex flex-col gap-4">
      <OAuthConnection
        strategy="oauth_google"    // Required: OAuth provider strategy
        icon="Google"              // Required: Icon name
        lastUsed={lastStrategy}    // Optional: Shows "Last used" badge
      >
        Continue with Google       {/* Required: Button text */}
      </OAuthConnection>
      
      <OAuthConnection
        strategy="oauth_github"
        icon="GitHub"
        lastUsed={lastStrategy}
      >
        Continue with GitHub
      </OAuthConnection>
      
      {/* Add more providers */}
      <OAuthConnection
        strategy="oauth_microsoft"  // Must match Clerk's strategy name exactly
        icon="Microsoft"
        lastUsed={lastStrategy}
      >
        Continue with Microsoft
      </OAuthConnection>
    </div>
  );
}

The OAuthConnection component requires these props:

Required:

  • strategy: OAuth provider identifier (e.g., "oauth_google", "oauth_github", "oauth_microsoft")
  • icon: Icon name for the button (e.g., "Google", "GitHub", "Microsoft")
    • Must exist in your SvgFinder component
  • children: Button text content (e.g., "Continue with Google")

Optional:

  • lastUsed: Last authentication strategy used (shows "Last used" badge when matching)
  • isSignedUp: Use on sign-up forms so the component starts a sign-up OAuth flow
  • className: Additional CSS classes for custom styling

How it works:

  • Calls signIn.authenticateWithRedirect() on sign-in forms
  • Calls signUp.authenticateWithRedirect() on sign-up forms when isSignedUp is set
  • Shows loading state with local loadingProvider state per provider during authentication
  • Displays "Last used" badge for the most recently used OAuth strategy
  • Redirects to /sso-callback to complete authentication
  • Uses BeatLoader spinner to indicate loading state

Available OAuth providers:

  • oauth_google - Google
  • oauth_github - GitHub
  • oauth_microsoft - Microsoft
  • oauth_apple - Apple
  • oauth_facebook - Facebook
  • oauth_linkedin - LinkedIn
  • oauth_twitter - Twitter

Enable each provider in Clerk Dashboard → Social Connections before adding the button.

Common Issues

Validation Not Working

  • Verify the schema is imported correctly in SignInForm.tsx
  • Check that react-hook-form resolver is configured with Zod
  • Ensure form fields have correct name attributes matching schema

Styling Not Applied

  • Clear browser cache and restart dev server
  • Check Tailwind CSS classes are valid
  • Verify globals.css is imported in root layout
  • Run npm run dev to rebuild Tailwind

Redirect Not Working

  • Verify the redirect URL is valid and accessible
  • Check middleware configuration in proxy.ts
  • Ensure the target route exists in your app

OAuth Buttons Not Showing

  • Verify OAuth providers are enabled in Clerk Dashboard
  • Check OAuthConnection component is imported correctly
  • Ensure Clerk environment variables are set

Next Steps

How is this guide ?

Last updated on

On this page