Auth Model
A production-grade authentication component featuring polished multi-provider support and a minimalist design.
Overview
The AuthModel is a refined, production-ready authentication component designed with the signature ZenBlocks minimalist aesthetic. It serves as a versatile building block for user onboarding, offering a clean layout that balances social authentication with secondary login methods like Email or Phone.
Designed for flexibility, it can function as a standalone landing page card, a sidebar element, or be seamlessly integrated into a modal environment.
Features
- Production-Ready Aesthetics: A clean, card-based layout with smooth transitions and subtle shadows.
- Multi-Provider Support: Easily configurable social authentication buttons with individual loading states.
- Smart Logic: Distinct separation between primary (social) and secondary (Email/Phone) authentication to reduce cognitive load.
- Standalone & Flexible: Works anywhere in your UI; highly customizable for various layouts.
- Built-in Branding: Optional ZenBlocks animated logo for a consistent brand experience.
- Accessible & Robust: Fully accessible with ARIA support and proper keyboard navigation.
Preview
ZenBlocks
To use ZenBlocks you must log into an existing account or create one using one of the options below
Installation
Method 1: CLI (Recommended)
Quickly add the component to your project:
npx shadcn@latest add https://zenblocks-three.vercel.app/r/auth-model.jsonMethod 2: Manual
-
Prerequisites
The
AuthModelrelies on standardshadcn/uicomponents. Ensure these are installed first:npx shadcn@latest add button input[!IMPORTANT] To use the
AuthModelinside a modal, you must first install the Modal Dialog component. This component is designed to be wrapped, not to include the modal logic itself. -
Install Dependencies
Install additional utility dependencies:
npm install lucide-react clsx tailwind-merge -
Copy the Source Code
Create
components/zenblocks/auth-model.tsxand paste the following:
Click to expand source
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
export interface AuthProvider {
id: string;
label: string;
icon?: React.ReactNode;
onSelect?: (id: string) => void;
disabled?: boolean;
}
export interface AuthModelProps {
title?: React.ReactNode;
description?: React.ReactNode;
providers: AuthProvider[];
loadingProviderId?: string | null;
footer?: React.ReactNode;
showDivider?: boolean;
dividerText?: string;
className?: string;
// Design specific props
showLogo?: boolean;
// Secondary Auth
secondaryPlaceholder?: string;
onSecondaryAction?: (value: string) => void;
secondaryContent?: React.ReactNode;
isSecondaryLoading?: boolean;
}
export const AuthModel = React.forwardRef<HTMLDivElement, AuthModelProps>(
function AuthModel(
{
title = "To use ZenBlocks you must log into an existing account or create one using one of the options below",
description,
providers,
loadingProviderId,
footer,
showDivider = false,
dividerText = "or continue with",
className,
showLogo = true,
secondaryPlaceholder = "Email or Phone",
onSecondaryAction,
secondaryContent,
isSecondaryLoading = false,
},
ref,
) {
const [secondaryValue, setSecondaryValue] = React.useState("");
return (
<div
ref={ref}
className={cn(
"w-full max-w-md overflow-hidden border border-zinc-800 bg-zinc-950 rounded-3xl transition-all duration-300 shadow-2xl shadow-indigo-500/10",
className,
)}
>
<div className="p-6 sm:p-8 flex flex-col items-center text-center">
{/* Logo Section - Original ZenBlocks Design */}
{showLogo && (
<div className="mb-6 sm:mb-8 mt-2 sm:mt-4 w-full flex flex-col items-center">
<div className="flex gap-1 items-center justify-center">
<div className="w-3.5 h-3.5 rounded-sm bg-white animate-pulse" />
<div className="w-3.5 h-3.5 rounded-sm bg-zinc-500" />
<div className="w-3.5 h-3.5 rounded-sm bg-zinc-700" />
</div>
<h1 className="text-2xl sm:text-3xl font-black tracking-tighter text-white mt-3 sm:mt-4 italic">
ZenBlocks
</h1>
</div>
)}
<div className="p-0 mb-6 sm:mb-8">
<h2 className="text-zinc-200 text-base sm:text-lg font-medium text-center justify-center leading-relaxed">
{title}
</h2>
{description && (
<p className="text-sm text-zinc-400 mt-2">{description}</p>
)}
</div>
<div className="w-full space-y-2.5 sm:space-y-3">
<div className="text-[10px] uppercase text-zinc-500 font-medium tracking-widest mb-4">
Best option below
</div>
{providers.map((provider) => {
const isLoading = loadingProviderId === provider.id;
return (
<Button
key={provider.id}
type="button"
variant="outline"
disabled={provider.disabled || isLoading}
onClick={() => provider.onSelect?.(provider.id)}
className="w-full h-11 sm:h-12 bg-zinc-900 border-zinc-800 text-zinc-200 hover:bg-zinc-800 hover:text-white transition-all rounded-xl gap-3 text-sm sm:text-base font-semibold"
aria-busy={isLoading}
>
{provider.icon && (
<span className="w-4 h-4 sm:w-5 sm:h-5 flex items-center justify-center">
{provider.icon}
</span>
)}
{isLoading ? "Please wait..." : provider.label}
</Button>
);
})}
</div>
{showDivider && (
<div className="w-full">
<div className="relative w-full my-6">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-zinc-800" />
</div>
<div className="relative flex justify-center text-[10px] uppercase text-zinc-500">
<span className="bg-zinc-950 px-3 tracking-wide">
{dividerText}
</span>
</div>
</div>
{secondaryContent || (
<div className="w-full space-y-3">
<Input
type="text"
placeholder={secondaryPlaceholder}
value={secondaryValue}
onChange={(e) => setSecondaryValue(e.target.value)}
className="h-11 sm:h-12 bg-zinc-900 border-zinc-800 text-zinc-200 rounded-xl focus-visible:ring-zinc-700"
/>
<Button
disabled={isSecondaryLoading}
onClick={() => onSecondaryAction?.(secondaryValue)}
className="w-full h-11 sm:h-12 bg-white text-black hover:bg-zinc-200 transition-all rounded-xl text-sm sm:text-base font-bold shadow-lg shadow-white/5"
aria-busy={isSecondaryLoading}
>
{isSecondaryLoading ? "Please wait..." : "Continue"}
</Button>
</div>
)}
</div>
)}
{footer ? (
<div className="mt-6 sm:mt-8 text-[10px] sm:text-xs text-zinc-500 text-center">
{footer}
</div>
) : (
<div className="mt-6 sm:mt-8 text-[10px] sm:text-xs text-zinc-500 max-w-[280px]">
By signing in, you accept the{" "}
<a
href="/terms"
className="text-zinc-400 hover:text-white underline underline-offset-2 transition-colors"
>
Terms of Service
</a>{" "}
and acknowledge our{" "}
<a
href="/privacy"
className="text-zinc-400 hover:text-white underline underline-offset-2 transition-colors"
>
Privacy Policy
</a>
.
</div>
)}
</div>
</div>
);
},
);Usage
import { AuthModel } from "@/components/zenblocks/auth-model";
import { Github } from "lucide-react";
export default function Example() {
const providers = [
{
id: "github",
label: "Continue with GitHub",
icon: <Github className="w-4 h-4" />,
onSelect: (id) => console.log(`Logging in with ${id}`),
},
];
return (
<AuthModel
providers={providers}
showDivider={true}
secondaryPlaceholder="Enter email"
onSecondaryAction={(email) => console.log(email)}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | "To use ZenBlocks..." | Component title heading |
description | ReactNode | - | Subtitle or secondary instructions |
providers | AuthProvider[] | - | Array of authentication providers |
loadingProviderId | string | null | - | The ID of the provider currently loading |
showDivider | boolean | false | Show divider and secondary auth section |
dividerText | string | "or continue with" | Text shown in the divider |
footer | ReactNode | - | Custom footer content |
showLogo | boolean | true | Show the ZenBlocks brand logo |
secondaryPlaceholder | string | "Email or Phone" | Placeholder for secondary input |
onSecondaryAction | (value: string) => void | - | Callback for secondary button |
isSecondaryLoading | boolean | false | Loading state for secondary button |
Accessibility
- ARIA Roles: Uses
aria-busyto communicate loading states to screen readers - Keyboard Navigation: Buttons and inputs are within the standard tab order
- Contrast: High-contrast Zinc-based color palette for maximum readability
- Semantic HTML: Proper use of headings and interactive elements
Performance
- Lazy Loading: Designed to work with dynamic imports for minimal bundle impact
- Memory Efficient: Uses
forwardReffor direct DOM access without overhead - Optimized Assets: CSS-based logo animation and shadows for GPU acceleration
- Clean States: Controlled inputs with minimal re-render cycles