Elements shadcn/ui shadcn/ui
The following examples demonstrate how to compose Clerk Elements with shadcn/ui to build custom sign-in and sign-up flows.
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
Install shadcn /ui components npx shadcn@latest add button card input label
Add the Icons
component from the shadcn/ui docs to an icons.tsx
file within your component/ui/
directory.
Add the following animations to your tailwind.config.js
file:
tailwind.config.js /** @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 .
app /sign-up /[[...sign-up]] /page.tsx 'use client'
import * as Clerk from '@clerk/elements/common'
import * as SignUp from '@clerk/elements/sign-up'
import Link from 'next/link'
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 >
)
}
app /sign-in /[[...sign-in]] /page.tsx 'use client'
import * as Clerk from '@clerk/elements/common'
import * as SignIn from '@clerk/elements/sign-in'
import Link from 'next/link'
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 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 >
)
}
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.
OTP Input < 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 >
)
}}
/>
Last updated on Dec 9, 2024