Blocksblock
Sign up
Name, email, and password registration with optional OAuth providers.
Preview
Open full screenInstallation
npx shadcn@latest add @particular/sign-upUsage
"use client"
import { LockIcon } from "lucide-react"
import { SignUpForm } from "@/components/sign-up-form"
import { toast } from "@/components/ui/toast"
function wait(ms: number) {
return new Promise<void>((resolve) => setTimeout(resolve, ms))
}
export function Demo() {
return (
<div className="flex min-h-svh items-center justify-center bg-muted/30 p-6">
<div className="w-full max-w-sm rounded-2xl border bg-card p-8 shadow-sm">
<SignUpForm
providers={[{ id: "sso", label: "SAML SSO", icon: <LockIcon /> }]}
onProviderSignUp={async (provider) => {
await wait(800)
toast.add({ title: `Redirecting to ${provider}…` })
}}
onSignUp={async ({ email }) => {
await wait(800)
if (email.endsWith("@taken.dev")) throw new Error("An account with this email already exists.")
toast.add({ title: "Account created", type: "success" })
}}
footer={
<>
Already have an account?{" "}
<a href="#" className="font-medium text-foreground underline underline-offset-4">
Sign in
</a>
</>
}
/>
</div>
</div>
)
}Source
"use client"
import * as React from "react"
import { cn } from "cn"
import { LogoMark } from "@/components/logo"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSeparator } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Spinner } from "@/components/ui/spinner"
export type SignUpProvider = { id: string; label: string; icon?: React.ReactNode }
export type SignUpFormProps = {
title?: string
description?: React.ReactNode
/** Create the account. Reject with an Error to show its message. */
onSignUp: (input: { name: string; email: string; password: string }) => Promise<void>
providers?: SignUpProvider[]
onProviderSignUp?: (providerId: string) => Promise<void>
minPasswordLength?: number
/** Legal copy or a link to sign in, shown under the form. */
footer?: React.ReactNode
className?: string
}
function messageOf(error: unknown) {
return error instanceof Error ? error.message : "Something went wrong. Try again."
}
function SignUpForm({
title = "Create your Particular.Tech account",
description = "One account for every Particular.Tech product.",
onSignUp,
providers = [],
onProviderSignUp,
minPasswordLength = 10,
footer,
className,
}: SignUpFormProps) {
const [pending, setPending] = React.useState<string | null>(null)
const [error, setError] = React.useState<string | null>(null)
const run = async (kind: string, action: () => Promise<void>) => {
setPending(kind)
setError(null)
try {
await action()
} catch (caught) {
setError(messageOf(caught))
} finally {
setPending(null)
}
}
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const form = new FormData(event.currentTarget)
const name = String(form.get("name") ?? "").trim()
const email = String(form.get("email") ?? "").trim()
const password = String(form.get("password") ?? "")
if (!name) {
setError("Enter your name.")
return
}
if (password.length < minPasswordLength) {
setError(`Use at least ${minPasswordLength} characters for your password.`)
return
}
void run("email", () => onSignUp({ name, email, password }))
}
return (
<div className={cn("mx-auto flex w-full max-w-sm flex-col gap-6", className)}>
<div className="flex flex-col items-center gap-3 text-center">
<LogoMark className="size-10" />
<h1 className="font-heading text-xl font-semibold tracking-tight">{title}</h1>
{description && <p className="text-sm text-muted-foreground">{description}</p>}
</div>
{providers.length > 0 && onProviderSignUp && (
<>
<div className="grid gap-2">
{providers.map((provider) => (
<Button
key={provider.id}
variant="outline"
size="lg"
disabled={pending !== null}
onClick={() => void run(`provider:${provider.id}`, () => onProviderSignUp(provider.id))}
>
{pending === `provider:${provider.id}` ? <Spinner data-icon="inline-start" /> : provider.icon}
Continue with {provider.label}
</Button>
))}
</div>
<FieldSeparator>or</FieldSeparator>
</>
)}
<form method="post" onSubmit={onSubmit}>
<FieldGroup>
<Field>
<FieldLabel htmlFor="sign-up-name">Name</FieldLabel>
<Input id="sign-up-name" name="name" autoComplete="name" placeholder="Ada Lovelace" required />
</Field>
<Field>
<FieldLabel htmlFor="sign-up-email">Email</FieldLabel>
<Input id="sign-up-email" name="email" type="email" autoComplete="email" placeholder="you@company.com" required />
</Field>
<Field>
<FieldLabel htmlFor="sign-up-password">Password</FieldLabel>
<Input id="sign-up-password" name="password" type="password" autoComplete="new-password" minLength={minPasswordLength} required />
<FieldDescription>At least {minPasswordLength} characters.</FieldDescription>
</Field>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button type="submit" size="lg" disabled={pending !== null}>
{pending === "email" && <Spinner data-icon="inline-start" />}
Create account
</Button>
</FieldGroup>
</form>
{footer && <div className="text-center text-xs text-muted-foreground">{footer}</div>}
</div>
)
}
export { SignUpForm }