shadcn/ui
The following examples demonstrate how to compose Clerk Elements with shadcn/ui to build custom sign-in and sign-up flows.
Before you start
To use these examples, you must first:
- Complete the shadcn/ui Next.js installation guide
- Install the Button, Card, Input, and Label components within your project
npx shadcn@latest add button card input label
- Add the
Icons
component from the shadcn/ui docs to anicons.tsx
file within yourcomponent/ui/
directory. - Add the following animations to your
tailwind.config.js
file:
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {
keyframes: {
'caret-blink': {
'0%,70%,100%': { opacity: '1' },
'20%,50%': { opacity: '0' },
},
},
animation: {
'caret-blink': 'caret-blink 1.25s ease-out infinite',
},
},
},
}
You must also configure the appropriate settings in Clerk:
- In the Clerk Dashboard, navigate to the SSO connections page.
- Ensure that Google and GitHub are enabled. If they are not in the list of connections, select the Add connection button, and select For all users. Enable Google and GitHub.
- Make sure you have your sign in and sign up URLs set in your environment variables.
Sign up
'use client'
import * as Clerk from '@clerk/elements/common'
import * as SignUp from '@clerk/elements/sign-up'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Icons } from '@/components/ui/icons'
import { cn } from '@/lib/utils'
export default function SignUpPage() {
return (
<div className="grid w-full grow items-center px-4 sm:justify-center">
<SignUp.Root>
<Clerk.Loading>
{(isGlobalLoading) => (
<>
<SignUp.Step name="start">
<Card className="w-full sm:w-96">
<CardHeader>
<CardTitle>Create your account</CardTitle>
<CardDescription>
Welcome! Please fill in the details to get started.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-y-4">
<div className="grid grid-cols-2 gap-x-4">
<Clerk.Connection name="github" asChild>
<Button
size="sm"
variant="outline"
type="button"
disabled={isGlobalLoading}
>
<Clerk.Loading scope="provider:github">
{(isLoading) =>
isLoading ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
<>
<Icons.gitHub className="mr-2 size-4" />
GitHub
</>
)
}
</Clerk.Loading>
</Button>
</Clerk.Connection>
<Clerk.Connection name="google" asChild>
<Button
size="sm"
variant="outline"
type="button"
disabled={isGlobalLoading}
>
<Clerk.Loading scope="provider:google">
{(isLoading) =>
isLoading ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
<>
<Icons.google className="mr-2 size-4" />
Google
</>
)
}
</Clerk.Loading>
</Button>
</Clerk.Connection>
</div>
<p className="flex items-center gap-x-3 text-sm text-muted-foreground before:h-px before:flex-1 before:bg-border after:h-px after:flex-1 after:bg-border">
or
</p>
<Clerk.Field name="emailAddress" className="space-y-2">
<Clerk.Label asChild>
<Label>Email address</Label>
</Clerk.Label>
<Clerk.Input type="email" required asChild>
<Input />
</Clerk.Input>
<Clerk.FieldError className="block text-sm text-destructive" />
</Clerk.Field>
<Clerk.Field name="password" className="space-y-2">
<Clerk.Label asChild>
<Label>Password</Label>
</Clerk.Label>
<Clerk.Input type="password" required asChild>
<Input />
</Clerk.Input>
<Clerk.FieldError className="block text-sm text-destructive" />
</Clerk.Field>
</CardContent>
<CardFooter>
<div className="grid w-full gap-y-4">
<SignUp.Captcha className="empty:hidden" />
<SignUp.Action submit asChild>
<Button disabled={isGlobalLoading}>
<Clerk.Loading>
{(isLoading) => {
return isLoading ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
'Continue'
)
}}
</Clerk.Loading>
</Button>
</SignUp.Action>
<Button variant="link" size="sm" asChild>
<Clerk.Link navigate="sign-in">Already have an account? Sign in</Clerk.Link>
</Button>
</div>
</CardFooter>
</Card>
</SignUp.Step>
<SignUp.Step name="continue">
<Card className="w-full sm:w-96">
<CardHeader>
<CardTitle>Continue registration</CardTitle>
</CardHeader>
<CardContent>
<Clerk.Field name="username" className="space-y-2">
<Clerk.Label>
<Label>Username</Label>
</Clerk.Label>
<Clerk.Input type="text" required asChild>
<Input />
</Clerk.Input>
<Clerk.FieldError className="block text-sm text-destructive" />
</Clerk.Field>
</CardContent>
<CardFooter>
<div className="grid w-full gap-y-4">
<SignUp.Action submit asChild>
<Button disabled={isGlobalLoading}>
<Clerk.Loading>
{(isLoading) => {
return isLoading ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
'Continue'
)
}}
</Clerk.Loading>
</Button>
</SignUp.Action>
</div>
</CardFooter>
</Card>
</SignUp.Step>
<SignUp.Step name="verifications">
<SignUp.Strategy name="email_code">
<Card className="w-full sm:w-96">
<CardHeader>
<CardTitle>Verify your email</CardTitle>
<CardDescription>
Use the verification link sent to your email address
</CardDescription>
</CardHeader>
<CardContent className="grid gap-y-4">
<div className="grid items-center justify-center gap-y-2">
<Clerk.Field name="code" className="space-y-2">
<Clerk.Label className="sr-only">Email address</Clerk.Label>
<div className="flex justify-center text-center">
<Clerk.Input
type="otp"
className="flex justify-center has-[:disabled]:opacity-50"
autoSubmit
render={({ value, status }) => {
return (
<div
data-status={status}
className={cn(
'relative flex size-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
{
'z-10 ring-2 ring-ring ring-offset-background':
status === 'cursor' || status === 'selected',
},
)}
>
{value}
{status === 'cursor' && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
</div>
)}
</div>
)
}}
/>
</div>
<Clerk.FieldError className="block text-center text-sm text-destructive" />
</Clerk.Field>
<SignUp.Action
asChild
resend
className="text-muted-foreground"
fallback={({ resendableAfter }) => (
<Button variant="link" size="sm" disabled>
Didn't receive a code? Resend (
<span className="tabular-nums">{resendableAfter}</span>)
</Button>
)}
>
<Button type="button" variant="link" size="sm">
Didn't receive a code? Resend
</Button>
</SignUp.Action>
</div>
</CardContent>
<CardFooter>
<div className="grid w-full gap-y-4">
<SignUp.Action submit asChild>
<Button disabled={isGlobalLoading}>
<Clerk.Loading>
{(isLoading) => {
return isLoading ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
'Continue'
)
}}
</Clerk.Loading>
</Button>
</SignUp.Action>
</div>
</CardFooter>
</Card>
</SignUp.Strategy>
</SignUp.Step>
</>
)}
</Clerk.Loading>
</SignUp.Root>
</div>
)
}
'use client'
import * as Clerk from '@clerk/elements/common'
import * as SignIn from '@clerk/elements/sign-in'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Icons } from '@/components/ui/icons'
export default function SignInPage() {
return (
<div className="grid w-full grow items-center px-4 sm:justify-center">
<SignIn.Root>
<Clerk.Loading>
{(isGlobalLoading) => (
<>
<SignIn.Step name="start">
<Card className="w-full sm:w-96">
<CardHeader>
<CardTitle>Sign in to Acme Co</CardTitle>
<CardDescription>Welcome back! Please sign in to continue</CardDescription>
</CardHeader>
<CardContent className="grid gap-y-4">
<div className="grid grid-cols-2 gap-x-4">
<Clerk.Connection name="github" asChild>
<Button
size="sm"
variant="outline"
type="button"
disabled={isGlobalLoading}
>
<Clerk.Loading scope="provider:github">
{(isLoading) =>
isLoading ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
<>
<Icons.gitHub className="mr-2 size-4" />
GitHub
</>
)
}
</Clerk.Loading>
</Button>
</Clerk.Connection>
<Clerk.Connection name="google" asChild>
<Button
size="sm"
variant="outline"
type="button"
disabled={isGlobalLoading}
>
<Clerk.Loading scope="provider:google">
{(isLoading) =>
isLoading ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
<>
<Icons.google className="mr-2 size-4" />
Google
</>
)
}
</Clerk.Loading>
</Button>
</Clerk.Connection>
</div>
<p className="flex items-center gap-x-3 text-sm text-muted-foreground before:h-px before:flex-1 before:bg-border after:h-px after:flex-1 after:bg-border">
or
</p>
<Clerk.Field name="identifier" className="space-y-2">
<Clerk.Label asChild>
<Label>Email address</Label>
</Clerk.Label>
<Clerk.Input type="email" required asChild>
<Input />
</Clerk.Input>
<Clerk.FieldError className="block text-sm text-destructive" />
</Clerk.Field>
</CardContent>
<CardFooter>
<div className="grid w-full gap-y-4">
<SignIn.Action submit asChild>
<Button disabled={isGlobalLoading}>
<Clerk.Loading>
{(isLoading) => {
return isLoading ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
'Continue'
)
}}
</Clerk.Loading>
</Button>
</SignIn.Action>
<Button variant="link" size="sm" asChild>
<Clerk.Link navigate="sign-up">
Don't have an account? Sign up
</Clerk.Link>
</Button>
</div>
</CardFooter>
</Card>
</SignIn.Step>
<SignIn.Step name="choose-strategy">
<Card className="w-full sm:w-96">
<CardHeader>
<CardTitle>Use another method</CardTitle>
<CardDescription>
Facing issues? You can use any of these methods to sign in.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-y-4">
<SignIn.SupportedStrategy name="email_code" asChild>
<Button type="button" variant="link" disabled={isGlobalLoading}>
Email code
</Button>
</SignIn.SupportedStrategy>
<SignIn.SupportedStrategy name="password" asChild>
<Button type="button" variant="link" disabled={isGlobalLoading}>
Password
</Button>
</SignIn.SupportedStrategy>
</CardContent>
<CardFooter>
<div className="grid w-full gap-y-4">
<SignIn.Action navigate="previous" asChild>
<Button disabled={isGlobalLoading}>
<Clerk.Loading>
{(isLoading) => {
return isLoading ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
'Go back'
)
}}
</Clerk.Loading>
</Button>
</SignIn.Action>
</div>
</CardFooter>
</Card>
</SignIn.Step>
<SignIn.Step name="verifications">
<SignIn.Strategy name="password">
<Card className="w-full sm:w-96">
<CardHeader>
<CardTitle>Check your email</CardTitle>
<CardDescription>
Enter the verification code sent to your email
</CardDescription>
<p className="text-sm text-muted-foreground">
Welcome back <SignIn.SafeIdentifier />
</p>
</CardHeader>
<CardContent className="grid gap-y-4">
<Clerk.Field name="password" className="space-y-2">
<Clerk.Label asChild>
<Label>Password</Label>
</Clerk.Label>
<Clerk.Input type="password" asChild>
<Input />
</Clerk.Input>
<Clerk.FieldError className="block text-sm text-destructive" />
</Clerk.Field>
</CardContent>
<CardFooter>
<div className="grid w-full gap-y-4">
<SignIn.Action submit asChild>
<Button disabled={isGlobalLoading}>
<Clerk.Loading>
{(isLoading) => {
return isLoading ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
'Continue'
)
}}
</Clerk.Loading>
</Button>
</SignIn.Action>
<SignIn.Action navigate="choose-strategy" asChild>
<Button type="button" size="sm" variant="link">
Use another method
</Button>
</SignIn.Action>
</div>
</CardFooter>
</Card>
</SignIn.Strategy>
<SignIn.Strategy name="email_code">
<Card className="w-full sm:w-96">
<CardHeader>
<CardTitle>Check your email</CardTitle>
<CardDescription>
Enter the verification code sent to your email
</CardDescription>
<p className="text-sm text-muted-foreground">
Welcome back <SignIn.SafeIdentifier />
</p>
</CardHeader>
<CardContent className="grid gap-y-4">
<Clerk.Field name="code">
<Clerk.Label className="sr-only">Email verification code</Clerk.Label>
<div className="grid gap-y-2 items-center justify-center">
<div className="flex justify-center text-center">
<Clerk.Input
type="otp"
autoSubmit
className="flex justify-center has-[:disabled]:opacity-50"
render={({ value, status }) => {
return (
<div
data-status={status}
className="relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md data-[status=selected]:ring-1 data-[status=selected]:ring-ring data-[status=cursor]:ring-1 data-[status=cursor]:ring-ring"
>
{value}
</div>
)
}}
/>
</div>
<Clerk.FieldError className="block text-sm text-destructive text-center" />
<SignIn.Action
asChild
resend
className="text-muted-foreground"
fallback={({ resendableAfter }) => (
<Button variant="link" size="sm" disabled>
Didn't receive a code? Resend (
<span className="tabular-nums">{resendableAfter}</span>)
</Button>
)}
>
<Button variant="link" size="sm">
Didn't receive a code? Resend
</Button>
</SignIn.Action>
</div>
</Clerk.Field>
</CardContent>
<CardFooter>
<div className="grid w-full gap-y-4">
<SignIn.Action submit asChild>
<Button disabled={isGlobalLoading}>
<Clerk.Loading>
{(isLoading) => {
return isLoading ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
'Continue'
)
}}
</Clerk.Loading>
</Button>
</SignIn.Action>
<SignIn.Action navigate="choose-strategy" asChild>
<Button size="sm" variant="link">
Use another method
</Button>
</SignIn.Action>
</div>
</CardFooter>
</Card>
</SignIn.Strategy>
</SignIn.Step>
</>
)}
</Clerk.Loading>
</SignIn.Root>
</div>
)
}
OTP input
The following example demonstrates how to make a one-time password (OTP) input with Clerk Elements. This example will only work if placed within a Step
in a sign-up or sign-in authentication flow, as shown in the sign-in and sign-up examples.
<Clerk.Input
type="otp"
className="flex justify-center has-[:disabled]:opacity-50"
autoSubmit
render={({ value, status }) => {
return (
<div
data-status={status}
className={cn(
'relative flex size-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
{
'z-10 ring-2 ring-ring ring-offset-background':
status === 'cursor' || status === 'selected',
},
)}
>
{value}
{status === 'cursor' && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
</div>
)}
</div>
)
}}
/>
Feedback
Last updated on