Email & Social Login Card
Centred sign-in card with social providers, a password reveal toggle and the autoComplete values password managers actually need.
Email first, then whatever that domain uses. The password field is absent rather than disabled when SSO is enforced, the button names the company it redirects to, and a personal address is a normal answer instead of an error.
components/auth-sso-domain.tsxnpx hoverlab add auth-sso-domain
Or over MCP, from your editor's agent — no account needed.
Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.
Start a page with this section — add more, order them, and leave with the page source.
Start with your work email — how you sign in depends on what your company has set up.
Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.
'use client'
/**
* <AuthSsoDomain> — email first, then whatever that email's company uses.
*
* Authentication had login, signup, OTP, forgot, reset and two-factor:
* six screens that all assume the account belongs to the person typing.
* The moment a customer has an IT department that assumption breaks, and
* the sign-in screen has to ask one question before it can know what to
* show. Nothing in the catalog asked it.
*
* THE ONE THING THIS GETS RIGHT THAT MOST DO NOT
*
* The password field is not disabled — it is *absent* until the domain is
* known, and when SSO is required it never appears at all, with the
* reason in its place. A greyed-out password box beside "your organisation
* requires SSO" invites ten minutes of trying to type into it, and every
* support ticket that follows starts with "the password field wasn't
* working". Removing an input is honest; disabling one is a riddle.
*
* THE DOMAIN IS ECHOED BACK
*
* "Continue with Meridian Foods SSO" rather than "Continue with SSO".
* Someone who mistypes their address at a company with two tenants finds
* out here, at the button, rather than after a redirect to an identity
* provider that shows them a login page for the wrong company.
*
* PERSONAL ADDRESSES ARE A NORMAL ANSWER
*
* A gmail.com address matching no tenant is the common case, not an
* error. It falls through to the password path with no warning colour and
* no scolding — the only difference is which second step appears.
*
* ACCESSIBILITY: one `<form>` with a real submit, so Enter works at every
* stage; the step change is announced through `aria-live`; the back
* control is a button that restores focus to the email field, because a
* step that cannot be undone from the keyboard is a trap.
*/
import * as React from 'react'
import { ArrowLeft, ArrowRight, Building2, KeyRound, Loader2, ShieldCheck } from 'lucide-react'
export interface SsoTenant {
/** Email domain, lower-case, without the @. */
domain: string
name: string
/** What the redirect actually goes to — worth naming before it happens. */
provider: string
/** When true, the password path is not offered at all. */
enforced?: boolean
}
export interface AuthSsoDomainProps {
tenants?: SsoTenant[]
className?: string
}
const DEFAULT_TENANTS: SsoTenant[] = [
{ domain: 'meridianfoods.com', name: 'Meridian Foods', provider: 'Okta', enforced: true },
{ domain: 'halden.co', name: 'Halden Group', provider: 'Microsoft Entra ID' },
]
type Step = 'email' | 'sso' | 'password'
export function AuthSsoDomain({ tenants = DEFAULT_TENANTS, className = '' }: AuthSsoDomainProps) {
/*
Per-instance prefix for every id this block emits.
The literals these replaced were a latent duplicate the moment the
block appeared twice on one document, and `aria-labelledby` on a
duplicated id resolves to the first match -- so the second copy was
labelled by the first copy's heading. Client component, so `useId` is
the right tool.
*/
const uid = React.useId()
/*
* Opens on the second step, with a domain already recognised.
*
* The first step is an email field, which is what every sign-in screen
* looks like and says nothing about what this one does. The answer —
* "Meridian Foods manages accounts on this domain, and there is no
* password field because they require SSO" — is the component. "Use a
* different email" walks back to the empty state.
*/
const [email, setEmail] = React.useState('you@meridianfoods.com')
const [step, setStep] = React.useState<Step>('sso')
const [tenant, setTenant] = React.useState<SsoTenant | null>(tenants[0] ?? null)
const [checking, setChecking] = React.useState(false)
const emailRef = React.useRef<HTMLInputElement>(null)
const domain = email.split('@')[1]?.trim().toLowerCase() ?? ''
function lookUp(event: React.FormEvent) {
event.preventDefault()
if (step !== 'email') return
setChecking(true)
/*
A real deployment asks the server here; the delay is kept because
the interesting design question is what the screen does while it
does not yet know, and a demo that answers instantly hides it.
*/
window.setTimeout(() => {
const match = tenants.find((t) => t.domain === domain) ?? null
setTenant(match)
setStep(match ? 'sso' : 'password')
setChecking(false)
}, 550)
}
function back() {
setStep('email')
setTenant(null)
/* Focus follows, or the back button is decorative for keyboard users. */
window.setTimeout(() => emailRef.current?.focus(), 0)
}
return (
<section className={`mx-auto w-full max-w-md px-4 py-16 sm:px-6 ${className}`}>
<div className="rounded-2xl border border-border bg-card p-6 sm:p-7">
<h1 className="text-xl font-semibold tracking-tight text-foreground">Sign in</h1>
<p className="mt-1 text-sm text-muted-foreground">
Start with your work email — how you sign in depends on what your company
has set up.
</p>
<form onSubmit={lookUp} className="mt-6 space-y-4">
<div>
<label htmlFor={`${uid}-sso-email`} className="block text-sm font-medium text-foreground">
Email
</label>
<input
id={`${uid}-sso-email`}
ref={emailRef}
type="email"
required
autoComplete="username"
value={email}
readOnly={step !== 'email'}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@meridianfoods.com"
className={`mt-1.5 h-10 w-full rounded-lg border border-field bg-background px-3 text-sm text-foreground outline-none transition placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring ${
step !== 'email' ? 'text-muted-foreground' : ''
}`}
/>
</div>
{/* The step change is announced; the visual change alone is not
available to a screen-reader user who just pressed Enter. */}
<div aria-live="polite">
{step === 'email' ? (
<button
type="submit"
disabled={checking || !email.includes('@')}
className="inline-flex h-10 w-full items-center justify-center gap-1.5 rounded-lg bg-primary px-4 text-sm font-semibold text-primary-foreground transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
{checking ? (
<>
<Loader2 aria-hidden className="h-4 w-4 motion-safe:animate-spin" />
Checking your organisation
</>
) : (
<>
Continue
<ArrowRight aria-hidden className="h-4 w-4 rtl:rotate-180" />
</>
)}
</button>
) : null}
{step === 'sso' && tenant ? (
<div className="space-y-3">
<div className="flex items-start gap-2.5 rounded-xl border border-border bg-muted/40 p-3">
<Building2 aria-hidden className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
<p className="text-xs leading-relaxed text-muted-foreground">
<strong className="font-semibold text-foreground">{tenant.name}</strong>{' '}
manages accounts on {tenant.domain}.{' '}
{tenant.enforced
? 'They require single sign-on, so there is no password to enter here.'
: 'You can use single sign-on or your password.'}
</p>
</div>
{/* Named, so a mistyped domain is caught before the redirect. */}
<button
type="button"
className="inline-flex h-10 w-full items-center justify-center gap-1.5 rounded-lg bg-primary px-4 text-sm font-semibold text-primary-foreground transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<ShieldCheck aria-hidden className="h-4 w-4" />
Continue with {tenant.name} SSO
</button>
<p className="text-center text-xs text-muted-foreground">
Takes you to {tenant.provider}
</p>
{/* Absent, not disabled, when SSO is enforced. */}
{!tenant.enforced ? (
<button
type="button"
onClick={() => setStep('password')}
className="inline-flex h-10 w-full items-center justify-center gap-1.5 rounded-lg border border-border bg-background px-4 text-sm font-medium text-foreground transition hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<KeyRound aria-hidden className="h-4 w-4" />
Use a password instead
</button>
) : null}
</div>
) : null}
{step === 'password' ? (
<div className="space-y-3">
<div>
<label
htmlFor={`${uid}-sso-password`}
className="block text-sm font-medium text-foreground"
>
Password
</label>
<input
id={`${uid}-sso-password`}
type="password"
autoComplete="current-password"
className="mt-1.5 h-10 w-full rounded-lg border border-field bg-background px-3 text-sm text-foreground outline-none transition focus-visible:ring-2 focus-visible:ring-ring"
/>
</div>
<button
type="button"
className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-primary px-4 text-sm font-semibold text-primary-foreground transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
Sign in
</button>
{/* A personal address is a normal answer, not a warning. */}
<p className="text-xs text-muted-foreground">
{tenant
? `${tenant.name} allows passwords as well as SSO.`
: `No organisation is set up for ${domain || 'that domain'} — this is an individual account.`}
</p>
</div>
) : null}
</div>
{step !== 'email' ? (
<button
type="button"
onClick={back}
className="inline-flex items-center gap-1.5 rounded text-xs font-medium text-muted-foreground transition hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<ArrowLeft aria-hidden className="h-3.5 w-3.5 rtl:rotate-180" />
Use a different email
</button>
) : null}
</form>
</div>
</section>
)
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/auth-sso-domain.tsx and import it where you need the section:
import { AuthSsoDomain } from '@/components/auth-sso-domain'Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.
| Prop | Type | Default |
|---|---|---|
tenants | SsoTenant[] | DEFAULT_TENANTS, className = '' |
className | string | — |
The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.
This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.
<!--
Email-First SSO Sign-In — markup from the Hoverlab catalog.
This is the block rendered once to HTML and wrapped as a component
file. It is not a port of the React source: the Tailwind classes carry
the design, which is the part that took the work, and they are the same
in every framework.
This block is interactive in React and the handlers are NOT here.
Buttons, toggles and menus render in their initial state and do
nothing until you wire them up.
-->
<section class="mx-auto w-full max-w-md px-4 py-16 sm:px-6 ">
<div class="rounded-2xl border border-border bg-card p-6 sm:p-7">
<h1 class="text-xl font-semibold tracking-tight text-foreground">Sign in</h1>
<p class="mt-1 text-sm text-muted-foreground">Start with your work email — how you sign in depends on what your company has set up.</p>
<form class="mt-6 space-y-4">
<div>
<label for="_R_0_-sso-email" class="block text-sm font-medium text-foreground">Email</label>
<input id="_R_0_-sso-email" type="email" required="" autoComplete="username" readOnly="" placeholder="you@meridianfoods.com" class="mt-1.5 h-10 w-full rounded-lg border border-field bg-background px-3 text-sm text-foreground outline-none transition placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring text-muted-foreground" value="you@meridianfoods.com" />
</div>
<div aria-live="polite">
<div class="space-y-3">
<div class="flex items-start gap-2.5 rounded-xl border border-border bg-muted/40 p-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-building2 lucide-building-2 mt-0.5 h-4 w-4 shrink-0 text-primary" aria-hidden="true">
<path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"></path>
<path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"></path>
<path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"></path>
<path d="M10 6h4"></path>
<path d="M10 10h4"></path>
<path d="M10 14h4"></path>
<path d="M10 18h4"></path>
</svg>
<p class="text-xs leading-relaxed text-muted-foreground"><strong class="font-semibold text-foreground">Meridian Foods</strong> manages accounts on meridianfoods.com. They require single sign-on, so there is no password to enter here.</p>
</div>
<button type="button" class="inline-flex h-10 w-full items-center justify-center gap-1.5 rounded-lg bg-primary px-4 text-sm font-semibold text-primary-foreground transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shield-check h-4 w-4" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"></path><path d="m9 12 2 2 4-4"></path></svg>Continue with Meridian Foods SSO</button>
<p class="text-center text-xs text-muted-foreground">Takes you to Okta</p>
</div>
</div>
<button type="button" class="inline-flex items-center gap-1.5 rounded text-xs font-medium text-muted-foreground transition hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left h-3.5 w-3.5 rtl:rotate-180" aria-hidden="true"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg>Use a different email</button>
</form>
</div>
</section>
What each framework gets across the whole catalog — effects convert properly; this rung is markup.
The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.
Want the whole screen instead of this one section? Open a page and copy it entire.
All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.
Browse AuthenticationCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
Centred sign-in card with social providers, a password reveal toggle and the autoComplete values password managers actually need.
Registration form beside a testimonial panel, with live password rules and a proof column that drops rather than stacks on mobile.
Six-box OTP entry that handles paste, backspace, arrow keys and iOS SMS autofill — the parts hand-rolled versions always miss.
Reset-link request with a sent state worded to avoid leaking whether an account exists.
Set-a-new-password form with a four-step strength meter, confirm matching and errors announced rather than only coloured.
Authenticator-code prompt with a backup-code escape hatch and an opt-in trusted-device checkbox that is off by default.
Passwordless sign-in that says what will land in the inbox and how long it lasts, before the address is typed.
What SSO actually covers, written for the person who has to configure it rather than the person who asked for it.