返回 Skills 目錄
clerk/skills包含需要注意的行為

SKILL DETAIL

clerk-custom-ui

clerk/skills/clerk-custom-ui

This skill covers two areas: custom authentication flows and appearance customization. Custom authentication flows allow you to build your own sign-in and sign-up UI using Clerk's hooks such as useSignIn and useSignUp. Appearance customization involves themes, colors, fonts, and CSS to adjust the look and branding of Clerk's pre-built components. The skill provides references for different SDK versions (Core 2 and current SDK v7+) and includes detailed guidance on appearance customization, including the appearance prop, themes (like dark, neobrutalism, shadcn, simple), variables (colors, fonts, spacing), and bring-your-own CSS. It also highlights common pitfalls such as using colorPrimary correctly and placing logoImageUrl inside options or layout.

安裝量 · 395查看來源

Installation

npx skills add https://github.com/clerk/skills --skill clerk-custom-ui

技能檔案

SKILL.md

最近同步 · 2026年8月29日

core-2/custom-sign-in.md
# Custom Sign-In Flow (Core 2)

> This document covers the **older SDK** (`@clerk/nextjs` v5–v6, `@clerk/clerk-react` v5–v6, `@clerk/clerk-expo` v1–v2). For the current SDK, see `core-3/custom-sign-in.md`.

Build a custom sign-in experience using the `useSignIn()` hook.

## Hook API

```typescript
import { useSignIn } from '@clerk/nextjs' // or @clerk/clerk-react, @clerk/clerk-expo

const { signIn, isLoaded, setActive } = useSignIn()
```

| Property | Type | Description |
|----------|------|-------------|
| `signIn` | `SignIn` | Sign-in object with methods |
| `isLoaded` | `boolean` | Whether the hook has loaded |
| `setActive` | `(params) => Promise` | Sets the active session |

## Sign-In Flow

### 1. Create Sign-In

```typescript
const result = await signIn.create({
  identifier: '[email protected]',
  password: 'securePassword123',
})
```

### 2. First Factor Verification

If additional verification is needed (email code, phone code):

```typescript
// Prepare first factor
await signIn.prepareFirstFactor({
  strategy: 'email_code', // or 'phone_code'
})

// Attempt first factor
const result = await signIn.attemptFirstFactor({
  strategy: 'email_code',
  code: '123456',
})
```

### 3. Second Factor (MFA)

If the sign-in requires MFA:

```typescript
// Prepare second factor
await signIn.prepareSecondFactor({
  strategy: 'email_code', // or 'phone_code'
})

// Attempt second factor
const result = await signIn.attemptSecondFactor({
  strategy: 'totp', // or 'email_code', 'phone_code', 'backup_code'
  code: '123456',
})
```

### 4. Finalize

Set the active session after successful authentication:

```typescript
await setActive({ session: signIn.createdSessionId })
```

### Password Reset

```typescript
// 1. Start reset flow
await signIn.create({ strategy: 'reset_password_email_code', identifier: '[email protected]' })

// or prepare after initial create:
await signIn.prepareFirstFactor({ strategy: 'reset_password_email_code' })

// 2. Verify reset code
await signIn.attemptFirstFactor({ strategy: 'reset_password_email_code', code: '123456' })

// 3. Set new password
await signIn.resetPassword({ password: 'newSecurePassword123' })
```

### SSO (OAuth)

```typescript
await signIn.authenticateWithRedirect({
  strategy: 'oauth_google', // or 'oauth_github', etc.
  redirectUrl: '/sso-callback',
  redirectUrlComplete: '/',
})
```

## Error Handling

Use try/catch with `isClerkAPIResponseError()`:

```typescript
import { isClerkAPIResponseError } from '@clerk/nextjs/errors'

try {
  await signIn.create({ identifier, password })
} catch (err) {
  if (isClerkAPIResponseError(err)) {
    err.errors.forEach((e) => {
      console.log(e.code)        // e.g. 'form_identifier_not_found'
      console.log(e.message)     // Human-readable message
      console.log(e.longMessage) // Detailed message
    })
  }
}
```

## Complete Example: Email/Password with MFA

```tsx
'use client'
import { useState } from 'react'
import { useSignIn } from '@clerk/nextjs'
import { isClerkAPIResponseError } from '@clerk/nextjs/errors'
import { useRouter } from 'next/navigation'

export default function SignInPage() {
  const { signIn, isLoaded, setActive } = useSignIn()
  const router = useRouter()

  const [identifier, setIdentifier] = useState('')
  const [password, setPassword] = useState('')
  const [mfaCode, setMfaCode] = useState('')
  const [step, setStep] = useState<'credentials' | 'mfa'>('credentials')
  const [error, setError] = useState('')

  if (!isLoaded) return <div>Loading...</div>

  async function handleSignIn(e: React.FormEvent) {
    e.preventDefault()
    setError('')

    try {
      const result = await signIn.create({ identifier, password })

      if (result.status === 'needs_second_factor') {
        setStep('mfa')
        return
      }

      if (result.status === 'complete') {
        await setActive({ session: result.createdSessionId })
        router.push('/')
      }
    } catch (err) {
      if (isClerkAPIResponseError(err)) {
        setError(err.errors[0]?.message || 'Sign in failed')
      }
    }
  }

  async function handleMFA(e: React.FormEvent) {
    e.preventDefault()
    setError('')

    try {
      const result = await signIn.attemptSecondFactor({
        strategy: 'totp',
        code: mfaCode,
      })

      if (result.status === 'complete') {
        await setActive({ session: result.createdSessionId })
        router.push('/')
      }
    } catch (err) {
      if (isClerkAPIResponseError(err)) {
        setError(err.errors[0]?.message || 'Verification failed')
      }
    }
  }

  if (step === 'mfa') {
    return (
      <form onSubmit={handleMFA}>
        <input
          type="text"
          value={mfaCode}
          onChange={(e) => setMfaCode(e.target.value)}
          placeholder="Enter MFA code"
        />
        {error && <p>{error}</p>}
        <button type="submit">Verify</button>
      </form>
    )
  }

  return (
    <form onSubmit={handleSignIn}>
      <input
        type="email"
        value={identifier}
        onChange={(e) => setIdentifier(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      {error && <p>{error}</p>}
      <button type="submit">Sign In</button>
    </form>
  )
}
```

## Docs

- [Custom sign-in flow](https://clerk.com/docs/custom-flows/overview)
- [useSignIn() reference](https://clerk.com/docs/references/react/use-sign-in)
core-2/custom-sign-up.md
# Custom Sign-Up Flow (Core 2)

> This document covers the **older SDK** (`@clerk/nextjs` v5–v6, `@clerk/clerk-react` v5–v6, `@clerk/clerk-expo` v1–v2). For the current SDK, see `core-3/custom-sign-up.md`.

Build a custom sign-up experience using the `useSignUp()` hook.

## Hook API

```typescript
import { useSignUp } from '@clerk/nextjs' // or @clerk/clerk-react, @clerk/clerk-expo

const { signUp, isLoaded, setActive } = useSignUp()
```

| Property | Type | Description |
|----------|------|-------------|
| `signUp` | `SignUp` | Sign-up object with methods |
| `isLoaded` | `boolean` | Whether the hook has loaded |
| `setActive` | `(params) => Promise` | Sets the active session |

## Sign-Up Flow

### 1. Create Sign-Up

```typescript
const result = await signUp.create({
  emailAddress: '[email protected]',
  password: 'securePassword123',
  firstName: 'Jane',  // optional
  lastName: 'Doe',    // optional
})
```

### 2. Prepare Verification

Send a verification code to the user's email or phone:

```typescript
await signUp.prepareVerification({
  strategy: 'email_code', // or 'phone_code', 'email_link'
})
```

### 3. Attempt Verification

Verify the code the user received:

```typescript
const result = await signUp.attemptVerification({
  strategy: 'email_code',
  code: '123456',
})
```

### 4. Finalize

Set the active session after successful sign-up:

```typescript
await setActive({ session: signUp.createdSessionId })
```

### SSO (OAuth)

```typescript
await signUp.authenticateWithRedirect({
  strategy: 'oauth_google',
  redirectUrl: '/sso-callback',
  redirectUrlComplete: '/',
})
```

## Error Handling

Use try/catch with `isClerkAPIResponseError()`:

```typescript
import { isClerkAPIResponseError } from '@clerk/nextjs/errors'

try {
  await signUp.create({ emailAddress, password })
} catch (err) {
  if (isClerkAPIResponseError(err)) {
    err.errors.forEach((e) => {
      console.log(e.code)        // e.g. 'form_password_pwned'
      console.log(e.message)     // Human-readable message
      console.log(e.longMessage) // Detailed message
    })
  }
}
```

## Complete Example: Email/Password with Email Verification

```tsx
'use client'
import { useState } from 'react'
import { useSignUp } from '@clerk/nextjs'
import { isClerkAPIResponseError } from '@clerk/nextjs/errors'
import { useRouter } from 'next/navigation'

export default function SignUpPage() {
  const { signUp, isLoaded, setActive } = useSignUp()
  const router = useRouter()

  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [code, setCode] = useState('')
  const [step, setStep] = useState<'register' | 'verify'>('register')
  const [error, setError] = useState('')

  if (!isLoaded) return <div>Loading...</div>

  async function handleRegister(e: React.FormEvent) {
    e.preventDefault()
    setError('')

    try {
      await signUp.create({ emailAddress: email, password })
      await signUp.prepareVerification({ strategy: 'email_code' })
      setStep('verify')
    } catch (err) {
      if (isClerkAPIResponseError(err)) {
        setError(err.errors[0]?.message || 'Sign up failed')
      }
    }
  }

  async function handleVerify(e: React.FormEvent) {
    e.preventDefault()
    setError('')

    try {
      const result = await signUp.attemptVerification({
        strategy: 'email_code',
        code,
      })

      if (result.status === 'complete') {
        await setActive({ session: result.createdSessionId })
        router.push('/')
      }
    } catch (err) {
      if (isClerkAPIResponseError(err)) {
        setError(err.errors[0]?.message || 'Verification failed')
      }
    }
  }

  if (step === 'verify') {
    return (
      <form onSubmit={handleVerify}>
        <p>Check your email for a verification code.</p>
        <input
          type="text"
          value={code}
          onChange={(e) => setCode(e.target.value)}
          placeholder="Verification code"
        />
        {error && <p>{error}</p>}
        <button type="submit">Verify Email</button>
      </form>
    )
  }

  return (
    <form onSubmit={handleRegister}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      {error && <p>{error}</p>}
      <button type="submit">Sign Up</button>
    </form>
  )
}
```

## Docs

- [Custom sign-up flow](https://clerk.com/docs/custom-flows/overview)
- [useSignUp() reference](https://clerk.com/docs/references/react/use-sign-up)
core-3/custom-sign-in.md
# Custom Sign-In Flow

Build a custom sign-in experience using the `useSignIn()` hook.

## Hook API

```typescript
import { useSignIn } from '@clerk/nextjs' // or @clerk/react, @clerk/expo

const { signIn, errors, fetchStatus } = useSignIn()
```

| Property | Type | Description |
|----------|------|-------------|
| `signIn` | `SignInFuture` | Sign-in object with namespaced methods |
| `errors` | `Errors<SignInFields>` | Structured error object |
| `fetchStatus` | `'idle' \| 'fetching'` | Network request status |

## Sign-In Methods

### Password

```typescript
const { error } = await signIn.password({
  identifier: '[email protected]',
  password: 'securePassword123',
})
```

### SSO (OAuth / Enterprise)

```typescript
const { error } = await signIn.sso({
  strategy: 'oauth_google', // or 'oauth_github', 'enterprise_sso', etc.
  redirectUrl: '/dashboard', // where to go after SSO completes
  redirectCallbackUrl: '/sso-callback', // intermediate callback route
})
```

### Passkey

```typescript
const { error } = await signIn.passkey({ flow: 'discoverable' })
```

### Web3

```typescript
const { error } = await signIn.web3({ strategy: 'web3_solana_signature' })
// or
const { error } = await signIn.web3({ strategy: 'web3_base_signature' })
```

### Ticket (Invitation link)

```typescript
const { error } = await signIn.ticket({ ticket: 'ticket_abc123' })
```

### Email Code

```typescript
// Send code (emailAddress is optional if a signIn already exists from a prior method call)
const { error } = await signIn.emailCode.sendCode({ emailAddress: '[email protected]' })

// Verify code
const { error } = await signIn.emailCode.verifyCode({ code: '123456' })
```

### Phone Code

```typescript
// Send code (phoneNumber is optional if a signIn already exists from a prior method call)
const { error } = await signIn.phoneCode.sendCode({ phoneNumber: '+12015551234' })

// Verify code
const { error } = await signIn.phoneCode.verifyCode({ code: '123456' })
```

## MFA (Second Factor)

A second factor is required when `signIn.status` is one of:
- `'needs_second_factor'` — user has MFA enabled (TOTP, backup codes, etc.)
- `'needs_client_trust'` — new device sign-in without MFA; requires email or phone code verification

```typescript
// TOTP (Authenticator app)
const { error } = await signIn.mfa.verifyTOTP({ code: '123456' })

// Backup code
const { error } = await signIn.mfa.verifyBackupCode({ code: 'backup-code-here' })

// Email code
const { error: sendErr } = await signIn.mfa.sendEmailCode()
const { error: verifyErr } = await signIn.mfa.verifyEmailCode({ code: '123456' })

// Phone code
const { error: sendErr } = await signIn.mfa.sendPhoneCode()
const { error: verifyErr } = await signIn.mfa.verifyPhoneCode({ code: '123456' })
```

## Password Reset

```typescript
// 1. Send reset code
const { error } = await signIn.resetPasswordEmailCode.sendCode()

// 2. Verify the code
const { error } = await signIn.resetPasswordEmailCode.verifyCode({ code: '123456' })

// 3. Submit new password
const { error } = await signIn.resetPasswordEmailCode.submitPassword({
  password: 'newSecurePassword123',
})
```

## Device Trust

When a user signs in with a valid password from a new device without MFA enabled, the sign-in status becomes `needs_client_trust`. This requires an additional verification step:

```typescript
if (signIn.status === 'needs_client_trust') {
  // Check supportedSecondFactors for available methods (email_code or phone_code)
  const factors = signIn.supportedSecondFactors
  // Use the appropriate mfa method to verify
}
```

## Finalizing Sign-In

After successful authentication, call `finalize()` to activate the session:

```typescript
await signIn.finalize({
  navigate: async ({ session, decorateUrl }) => {
    const destination = session.currentTask
      ? `/sign-in/tasks/${session.currentTask.key}`
      : '/'
    const url = decorateUrl(destination)
    // decorateUrl may return an absolute URL for Safari ITP
    if (url.startsWith('http')) {
      window.location.href = url
    } else {
      router.push(url)
    }
  },
})
```

- `decorateUrl(path)` — decorates the URL with session info (required to support Safari's Intelligent Tracking Prevention). May return an absolute URL.
- `session.currentTask` — check for pending session tasks before redirecting

### Reset State

Clear local sign-in state and start over:

```typescript
signIn.reset()
```

## Error Handling

All methods return `Promise<{ error: ClerkError | null }>`. Errors are also available reactively on the hook:

```typescript
const { signIn, errors } = useSignIn()

// Field-level errors
errors?.fields?.identifier // { code, message, longMessage? }
errors?.fields?.password   // { code, message, longMessage? }
errors?.fields?.code       // { code, message, longMessage? }

// Global errors (not tied to a field)
errors?.global // ClerkGlobalHookError[] | null

// Raw error array
errors?.raw // ClerkError[] | null
```

## Complete Example: Email/Password with MFA

From [the docs](https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication). Supports SMS verification codes, authenticator app (TOTP), and backup codes.

```tsx
'use client'

import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'

export default function Page() {
  const { signIn, errors, fetchStatus } = useSignIn()
  const router = useRouter()

  const handleSubmit = async (formData: FormData) => {
    const emailAddress = formData.get('email') as string
    const password = formData.get('password') as string

    await signIn.password({
      emailAddress,
      password,
    })

    // If you're using the authenticator app strategy, remove this check.
    if (signIn.status === 'needs_second_factor') {
      await signIn.mfa.sendPhoneCode()
    }

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: ({ session, decorateUrl }) => {
          if (session?.currentTask) {
            // Handle pending session tasks
            // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
            console.log(session?.currentTask)
            return
          }

          const url = decorateUrl('/')
          if (url.startsWith('http')) {
            window.location.href = url
          } else {
            router.push(url)
          }
        },
      })
    }
  }

  const handleMFAVerification = async (formData: FormData) => {
    const code = formData.get('code') as string
    const useBackupCode = formData.get('useBackupCode') === 'on'

    if (useBackupCode) {
      await signIn.mfa.verifyBackupCode({ code })
    } else {
      await signIn.mfa.verifyPhoneCode({ code })
      // If you're using the authenticator app strategy, use the following method instead:
      // await signIn.mfa.verifyTOTP({ code })
    }

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: ({ session, decorateUrl }) => {
          if (session?.currentTask) {
            // Handle pending session tasks
            // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
            console.log(session?.currentTask)
            return
          }

          const url = decorateUrl('/')
          if (url.startsWith('http')) {
            window.location.href = url
          } else {
            router.push(url)
          }
        },
      })
    }
  }

  if (signIn.status === 'needs_second_factor') {
    return (
      <div>
        <h1>Verify your account</h1>
        <form action={handleMFAVerification}>
          <div>
            <label htmlFor="code">Code</label>
            <input id="code" name="code" type="text" />
            {errors.fields.code && <p>{errors.fields.code.message}</p>}
          </div>
          <div>
            <label>
              Use backup code
              <input type="checkbox" name="useBackupCode" />
            </label>
          </div>
          <button type="submit" disabled={fetchStatus === 'fetching'}>
            Verify
          </button>
        </form>
      </div>
    )
  }

  return (
    <>
      <h1>Sign in</h1>
      <form action={handleSubmit}>
        <div>
          <label htmlFor="email">Enter email address</label>
          <input id="email" name="email" type="email" />
          {errors.fields.identifier && <p>{errors.fields.identifier.message}</p>}
        </div>
        <div>
          <label htmlFor="password">Enter password</label>
          <input id="password" name="password" type="password" />
          {errors.fields.password && <p>{errors.fields.password.message}</p>}
        </div>
        <button type="submit" disabled={fetchStatus === 'fetching'}>
          Continue
        </button>
      </form>
      {errors && <p>{JSON.stringify(errors, null, 2)}</p>}
    </>
  )
}
```

## Docs

- [Custom sign-in flow](https://clerk.com/docs/custom-flows/overview)
- [MFA custom flow](https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication)
- [useSignIn() reference](https://clerk.com/docs/references/react/use-sign-in)
core-3/custom-sign-up.md
# Custom Sign-Up Flow

Build a custom sign-up experience using the `useSignUp()` hook.

## Hook API

```typescript
import { useSignUp } from '@clerk/nextjs' // or @clerk/react, @clerk/expo

const { signUp, errors, fetchStatus } = useSignUp()
```

| Property | Type | Description |
|----------|------|-------------|
| `signUp` | `SignUpFuture` | Sign-up object with namespaced methods |
| `errors` | `Errors<SignUpFields>` | Structured error object |
| `fetchStatus` | `'idle' \| 'fetching'` | Network request status |

## Sign-Up Methods

### Password (Email/Password)

```typescript
const { error } = await signUp.password({
  emailAddress: '[email protected]',
  password: 'securePassword123',
  firstName: 'Jane',  // optional
  lastName: 'Doe',    // optional
})
```

### SSO (OAuth)

```typescript
const { error } = await signUp.sso({
  strategy: 'oauth_google', // or 'oauth_github', etc.
  redirectUrl: '/dashboard', // where to go after SSO completes
  redirectCallbackUrl: '/sso-callback', // intermediate callback route
})
```

### Web3

```typescript
const { error } = await signUp.web3({ strategy: 'web3_solana_signature' })
```

### Update (add fields to existing sign-up)

Use `update()` to add optional fields (name, metadata, legal acceptance, locale) to an existing sign-up before finalization.

```typescript
const { error } = await signUp.update({
  firstName: 'Jane',
  lastName: 'Doe',
  unsafeMetadata: { referralSource: 'twitter' },
  legalAccepted: true,
})
```

## Email / Phone Verification

After creating a sign-up, verify the user's email or phone:

### Email Code

```typescript
// Send verification code
const { error } = await signUp.verifications.sendEmailCode()

// Verify the code
const { error } = await signUp.verifications.verifyEmailCode({ code: '123456' })
```

### Phone Code

```typescript
// Send verification code
const { error } = await signUp.verifications.sendPhoneCode()

// Verify the code
const { error } = await signUp.verifications.verifyPhoneCode({ code: '123456' })
```

### Email Link

```typescript
// verificationUrl: where the user lands after clicking the email link (relative or absolute)
const { error } = await signUp.verifications.sendEmailLink({ verificationUrl: '/verify' })
// User clicks the link in their email to verify
```

## Finalizing Sign-Up

After successful sign-up and verification, call `finalize()` to activate the session:

```typescript
await signUp.finalize({
  navigate: async ({ session, decorateUrl }) => {
    const destination = session.currentTask
      ? `/sign-up/tasks/${session.currentTask.key}`
      : '/'
    const url = decorateUrl(destination)
    // decorateUrl may return an absolute URL for Safari ITP
    if (url.startsWith('http')) {
      window.location.href = url
    } else {
      router.push(url)
    }
  },
})
```

### Transferable Sign-Ups

If `signUp.isTransferable` is `true`, the identifier matches an existing user and the sign-up should be transferred to a sign-in flow. This involves coordinating between sign-up and sign-in resources. See the [transferable sign-up docs](https://clerk.com/docs/custom-flows/overview) for the full implementation.

### Reset State

Clear local sign-up state and start over:

```typescript
signUp.reset()
```

## Error Handling

All methods return `Promise<{ error: ClerkError | null }>`. Errors are also available reactively on the hook:

```typescript
const { signUp, errors } = useSignUp()

// Field-level errors
errors?.fields?.emailAddress // { code, message, longMessage? }
errors?.fields?.password     // { code, message, longMessage? }
errors?.fields?.firstName    // { code, message, longMessage? }
errors?.fields?.lastName     // { code, message, longMessage? }
errors?.fields?.phoneNumber  // { code, message, longMessage? }
errors?.fields?.username     // { code, message, longMessage? }
errors?.fields?.code         // { code, message, longMessage? }

// Global errors
errors?.global // ClerkGlobalHookError[] | null

// Raw error array
errors?.raw // ClerkError[] | null
```

## Complete Example: Phone OTP Sign-Up

From [the docs](https://clerk.com/docs/guides/development/custom-flows/authentication/email-sms-otp). Uses phone OTP with inline comments for adapting to email OTP.

```tsx
'use client'

import * as React from 'react'
import { useAuth, useSignUp } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'

export default function SignUpPage() {
  const { signUp, errors, fetchStatus } = useSignUp()
  const { isSignedIn } = useAuth()
  const router = useRouter()

  const handleSubmit = async (formData: FormData) => {
    // For email OTP: collect the email address instead of the phone number
    const phoneNumber = formData.get('phoneNumber') as string

    // For email OTP: change create({ phoneNumber }) to create({ emailAddress })
    const error = await signUp.create({ phoneNumber })

    // For email OTP: change sendPhoneCode() to sendEmailCode()
    if (!error) await signUp.verifications.sendPhoneCode()
  }

  const handleVerify = async (formData: FormData) => {
    const code = formData.get('code') as string

    // For email OTP: change verifyPhoneCode() to verifyEmailCode()
    await signUp.verifications.verifyPhoneCode({ code })

    if (signUp.status === 'complete') {
      await signUp.finalize({
        navigate: ({ session, decorateUrl }) => {
          if (session?.currentTask) {
            // Handle pending session tasks
            // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
            console.log(session?.currentTask)
            return
          }

          const url = decorateUrl('/')
          if (url.startsWith('http')) {
            window.location.href = url
          } else {
            router.push(url)
          }
        },
      })
    }
  }

  if (signUp.status === 'complete' || isSignedIn) {
    return null
  }

  if (
    signUp.status === 'missing_requirements' &&
    // For email OTP: check for phone_number instead of email_address
    signUp.unverifiedFields.includes('phone_number') &&
    signUp.missingFields.length === 0
  ) {
    return (
      <>
        <h1>Verify your account</h1>
        <form action={handleVerify}>
          <div>
            <label htmlFor="code">Code</label>
            <input id="code" name="code" type="text" />
          </div>
          {errors.fields.code && <p>{errors.fields.code.message}</p>}
          <button type="submit" disabled={fetchStatus === 'fetching'}>
            Verify
          </button>
        </form>
        {/* For email OTP: change sendPhoneCode() to sendEmailCode() */}
        <button onClick={() => signUp.verifications.sendPhoneCode()}>I need a new code</button>
      </>
    )
  }

  return (
    <>
      <h1>Sign up</h1>
      <form action={handleSubmit}>
        {/* For email OTP: collect the emailAddress instead */}
        <div>
          <label htmlFor="phoneNumber">Phone number</label>
          <input id="phoneNumber" name="phoneNumber" type="tel" />
          {errors.fields.phoneNumber && <p>{errors.fields.phoneNumber.message}</p>}
        </div>
        <button type="submit" disabled={fetchStatus === 'fetching'}>
          Continue
        </button>
      </form>
      {errors && <p>{JSON.stringify(errors, null, 2)}</p>}

      {/* Required for sign-up flows. Clerk's bot sign-up protection is enabled by default */}
      <div id="clerk-captcha" />
    </>
  )
}
```

## Docs

- [Custom sign-up flow](https://clerk.com/docs/custom-flows/overview)
- [Email/phone OTP custom flow](https://clerk.com/docs/guides/development/custom-flows/authentication/email-sms-otp)
- [useSignUp() reference](https://clerk.com/docs/references/react/use-sign-up)
core-3/show-component.md
# `<Show>` Component

The `<Show>` component conditionally renders content based on authentication state, roles, permissions, billing plans, and features.

> **Core 2 ONLY (skip if current SDK):** The `<Show>` component does not exist in Core 2. Use `<SignedIn>`, `<SignedOut>`, and `<Protect>` instead. See migration table below.

## Import

```typescript
import { Show } from '@clerk/nextjs'       // Next.js
import { Show } from '@clerk/react'         // React
import { Show } from '@clerk/react-router'  // React Router
import { Show } from '@clerk/expo'          // Expo
```

## Props

| Prop | Type | Description |
|------|------|-------------|
| `when` | `string \| object \| function` | Condition for rendering children |
| `fallback?` | `ReactNode` | Content shown when condition fails |
| `treatPendingAsSignedOut?` | `boolean` | Treat pending sessions as signed-out (default: `true`) |

## `when` Prop Variants

### Authentication State

```tsx
// Show content only when signed in
<Show when="signed-in">
  <p>Welcome back!</p>
</Show>

// Show content only when signed out
<Show when="signed-out">
  <p>Please sign in.</p>
</Show>
```

### Role Check

```tsx
<Show when={{ role: 'org:admin' }}>
  <AdminPanel />
</Show>
```

### Permission Check

```tsx
<Show when={{ permission: 'org:billing:manage' }}>
  <BillingSettings />
</Show>
```

### Billing Feature Check

```tsx
<Show when={{ feature: 'widgets' }}>
  <WidgetBuilder />
</Show>
```

### Billing Plan Check

```tsx
<Show when={{ plan: 'gold' }}>
  <PremiumContent />
</Show>
```

### Custom Condition (Function)

```tsx
<Show when={(has) => has({ role: 'org:admin' }) || has({ permission: 'org:billing:manage' })}>
  <SettingsPanel />
</Show>
```

## Fallback Content

Show alternative content when the condition fails:

```tsx
<Show when="signed-in" fallback={<p>Please sign in to continue.</p>}>
  <Dashboard />
</Show>
```

## Session Tasks and Pending State

The `treatPendingAsSignedOut` prop controls how pending sessions (sessions with incomplete tasks) are handled:

```tsx
// Default: pending sessions are treated as signed-out
<Show when="signed-in" treatPendingAsSignedOut>
  <Dashboard />
</Show>

// Treat pending sessions as signed-in (e.g., to show task completion UI)
<Show when="signed-in" treatPendingAsSignedOut={false}>
  <TaskCompletionFlow />
</Show>
```

## Security Caveat

**`<Show>` only visually hides content** — it remains in browser source. It is not a security boundary. For protecting sensitive data, always verify authentication server-side with `auth()` or use `auth.protect()` in middleware.

## Migration from Core 2

| Core 2 | Current |
|--------|---------|
| `<SignedIn>` | `<Show when="signed-in">` |
| `<SignedOut>` | `<Show when="signed-out">` |
| `<Protect role="org:admin">` | `<Show when={{ role: 'org:admin' }}>` |
| `<Protect permission="org:billing:manage">` | `<Show when={{ permission: 'org:billing:manage' }}>` |
| `<Protect condition={(has) => expr}>` | `<Show when={(has) => expr}>` |
| `<Protect fallback={...}>` | `<Show when={...} fallback={...}>` |
| *(no equivalent)* | `<Show when={{ feature: 'widgets' }}>` |
| *(no equivalent)* | `<Show when={{ plan: 'gold' }}>` |

## Docs

- [Show component reference](https://clerk.com/docs/components/control/show)
SKILL.md
---
name: clerk-custom-ui
description: Custom authentication flows and component appearance - hooks (useSignIn,
  useSignUp), themes, colors, fonts, CSS. Use for custom sign-in/sign-up flows, appearance
  styling, visual customization, branding.
allowed-tools: WebFetch
license: MIT
metadata:
  author: clerk
  version: 2.3.0
---

# Custom UI

> **Prerequisite**: Ensure `ClerkProvider` wraps your app. See `clerk-setup` skill.
>
> **Version**: Check `package.json` for the SDK version — see `clerk` skill for the version table. This determines which custom flow references to use below.

This skill covers two areas:
1. **Custom authentication flows** — build your own sign-in/sign-up UI with hooks
2. **Appearance customization** — theme, style, and brand Clerk's pre-built components

## What Do You Need?

| Task | Reference |
|------|-----------|
| Custom sign-in (Core 2 / LTS) | core-2/custom-sign-in.md |
| Custom sign-up (Core 2 / LTS) | core-2/custom-sign-up.md |
| Custom sign-in (Current SDK v7+) | core-3/custom-sign-in.md |
| Custom sign-up (Current SDK v7+) | core-3/custom-sign-up.md |
| Show component pattern (Current SDK) | core-3/show-component.md |

## Custom Flow References

| Task | Core 2 | Current |
|------|--------|---------|
| Custom sign-in (useSignIn) | `core-2/custom-sign-in.md` | `core-3/custom-sign-in.md` |
| Custom sign-up (useSignUp) | `core-2/custom-sign-up.md` | `core-3/custom-sign-up.md` |
| `<Show>` component | *(use `<SignedIn>`, `<SignedOut>`, `<Protect>`)* | `core-3/show-component.md` |

---

## Appearance Customization

Appearance customization applies to both Core 2 and the current SDK.

### Component Customization Options

| Task | Documentation |
|------|---------------|
| Appearance prop overview | https://clerk.com/docs/nextjs/guides/customizing-clerk/appearance-prop/overview |
| Options (structure, logo, buttons) | https://clerk.com/docs/nextjs/guides/customizing-clerk/appearance-prop/layout |
| Themes (pre-built dark/light) | https://clerk.com/docs/nextjs/guides/customizing-clerk/appearance-prop/themes |
| Variables (colors, fonts, spacing) | https://clerk.com/docs/nextjs/guides/customizing-clerk/appearance-prop/variables |
| CAPTCHA configuration | https://clerk.com/docs/nextjs/guides/customizing-clerk/appearance-prop/captcha |
| Bring your own CSS | https://clerk.com/docs/nextjs/guides/customizing-clerk/appearance-prop/bring-your-own-css |

### Appearance Pattern

```typescript
<SignIn
  appearance={{
    variables: {
      colorPrimary: '#0000ff',
      borderRadius: '0.5rem',
    },
    options: {
      logoImageUrl: '/logo.png',
      socialButtonsVariant: 'iconButton',
    },
  }}
/>
```

> **Core 2 ONLY (skip if current SDK):** The `options` property was named `layout`. Use `layout: { logoImageUrl: '...', socialButtonsVariant: '...' }` instead of `options`.

### variables (colors, typography, borders)

| Property | Description |
|----------|-------------|
| `colorPrimary` | Primary color throughout |
| `colorBackground` | Background color |
| `borderRadius` | Border radius (default: `0.375rem`) |

**Opacity change:** `colorRing` and `colorModalBackdrop` now render at full opacity. Use explicit `rgba()` values if you need transparency.

> **Core 2 ONLY (skip if current SDK):** `colorRing` and `colorModalBackdrop` rendered at 15% opacity by default.

### options (structure, logo, social buttons)

| Property | Description |
|----------|-------------|
| `logoImageUrl` | URL to custom logo |
| `socialButtonsVariant` | `'blockButton'` \| `'iconButton'` \| `'auto'` |
| `socialButtonsPlacement` | `'top'` \| `'bottom'` |
| `showOptionalFields` | Show optional fields (default: `false`) |

> **Core 2 ONLY (skip if current SDK):** This property is called `layout`, not `options`. Also, `showOptionalFields` defaulted to `true`.

### Themes

Install themes from `@clerk/ui`:

```bash
npm install @clerk/ui
```

```typescript
import { dark } from '@clerk/ui/themes'

<ClerkProvider appearance={{ theme: dark }} />
```

> **Core 2 ONLY (skip if current SDK):** Themes are imported from `@clerk/themes` (install `@clerk/themes` instead of `@clerk/ui`). Example: `import { dark } from '@clerk/themes'`.

**Theme stacking** — pass an array where the last theme takes precedence:

```typescript
import { dark, neobrutalism } from '@clerk/ui/themes'

<ClerkProvider appearance={{ theme: [dark, neobrutalism] }} />
```

**CSS `color-scheme` support** — the default theme respects the CSS `color-scheme` property for automatic light/dark mode toggling.

**Available themes:** `dark`, `neobrutalism`, `shadcn`, `simple`

#### shadcn Theme

> **IMPORTANT:** If the project has `components.json` (shadcn/ui installed), ALWAYS apply the shadcn theme as the FIRST customization step. This ensures Clerk components visually match the existing shadcn design system. Skip this only if the user explicitly requests a different theme.

**If the project uses shadcn/ui** (check for `components.json` in the project root), **always use the shadcn theme**:

```typescript
import { shadcn } from '@clerk/ui/themes'

<ClerkProvider appearance={{ theme: shadcn }} />
```

Also import shadcn CSS in your global styles:
```css
@import 'tailwindcss';
@import '@clerk/ui/themes/shadcn.css';
```

> **Core 2 ONLY (skip if current SDK):** Import from `@clerk/themes` and `@clerk/themes/shadcn.css`:
> ```typescript
> import { shadcn } from '@clerk/themes'
> ```
> ```css
> @import '@clerk/themes/shadcn.css';
> ```

## Workflow

1. Identify customization needs (custom flow or appearance)
2. For custom flows: check SDK version → read appropriate `core-2/` or `core-3/` reference
3. For appearance: WebFetch the appropriate documentation from table above
4. Apply appearance prop to your Clerk components or build custom flow with hooks

## Common Pitfalls

| Issue | Solution |
|-------|----------|
| Colors not applying | Use `colorPrimary` not `primaryColor` |
| Logo not showing | Put `logoImageUrl` inside `options: {}` (or `layout: {}` in Core 2) |
| Social buttons wrong | Add `socialButtonsVariant: 'iconButton'` in `options` (or `layout` in Core 2) |
| Styling not working | Use appearance prop, not direct CSS (unless with bring-your-own-css) |
| Hook returns different shape | Check SDK version — Core 2 and current have completely different `useSignIn`/`useSignUp` APIs |

## See Also

- `clerk-setup` - Initial Clerk install
- `clerk-nextjs-patterns` - Next.js patterns
- `clerk-orgs` - B2B organizations
clerk-custom-ui · 熱門 Agent Skills | Mengbi