---
name: sign-in
title: Sign In
url: https://ui.frontic.com/components/sections/sign-in.md
tier: sections
install: npx @frontic/ui add sign-in
files:
  - components/sections/sign-in/SignIn.vue
  - components/sections/sign-in/index.ts
registryDependencies:
  - button
  - card
  - field
  - input
  - input-otp
dependencies:
  - "@lucide/vue"
---

# Sign In

Passwordless sign-in card: the email the code went to, a one-time code that enables the submit once complete, and passkey or password alternatives.

## Install

```sh
npx @frontic/ui add sign-in
```

Built from: [Button](https://ui.frontic.com/components/ui/button.md), [Card](https://ui.frontic.com/components/ui/card.md), [Field](https://ui.frontic.com/components/ui/field.md), [Input](https://ui.frontic.com/components/ui/input.md), [Input OTP](https://ui.frontic.com/components/ui/input-otp.md)

npm packages: `@lucide/vue`

## Examples

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { SignIn } from '@/components/sections/sign-in'

const code = ref('4817')
</script>

<template>
  <div class="mx-auto w-full max-w-md">
    <SignIn v-model:code="code" email="lena.berg@example.com" code-hint="Didn't get it? Resend in 0:42." />
  </div>
</template>
```

## API reference

### SignIn

Lets the shopper sign in with an emailed code, a passkey or a password.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `email` (required) | `string` |  | The address the code was sent to. |
| `codeLength` | `number` | `6` | Digits in the emailed code; the slots split into two halves. |
| `codeHint` | `string` |  | Under the code, e.g. when the code can be sent again. |
| `title` | `string` | `'Sign in'` | Shown as the card's title. |
| `description` | `string` | `'No password needed. We email you a code.'` | Shown under the title. |
| `emailLabel` | `string` | `'Email'` | Shown above the email field. |
| `changeLabel` | `string` | `'Change'` | Shown on the button that lets the shopper change the email address. |
| `codeLabel` | `string` | `'Verification code'` | Shown above the code slots. |
| `submitLabel` | `string` | `'Verify and sign in'` | Shown on the button that submits the code. |
| `separatorLabel` | `string` | `'or'` | Shown on the divider above the other sign-in options. |
| `passkeyLabel` | `string` | `'Use a passkey'` | Shown on the button that signs in with a passkey. |
| `passwordLabel` | `string` | `'Sign in with password'` | Shown on the button that switches to signing in with a password. |
| `surface` | `'raised' \| 'bare'` | `'raised'` | Bare drops the edge, shadow and inset, for a section placed inside another Card. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `code` | `string` | `''` | The code entered so far; submitted once all its digits are filled. |

| Event | Payload | Description |
| --- | --- | --- |
| `change` |  | Fires when the customer asks to change the email address the code was sent to. |
| `submit` | `code: string` | Fires with the entered code when the customer completes or submits it. |
| `password` |  | Fires when the customer chooses to sign in with a password instead. |
| `update:code` | `value: string` | Fires with the new value when `code` changes; `v-model:code` binds it. |
| `passkey` |  | Fires when the customer chooses to sign in with a passkey. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-surface` | `'raised' \| 'bare'` | Carries the `surface` prop. |

## Source

`components/sections/sign-in/SignIn.vue`

```vue
<script lang="ts">
/** Lets the shopper sign in with an emailed code, a passkey or a password. */
export default {}
</script>

<script setup lang="ts">
import { KeyRoundIcon, LockIcon } from '@lucide/vue'

import type { HTMLAttributes } from 'vue'
import { computed, useId } from 'vue'
import { Button } from '@library/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@library/components/ui/card'
import { Field, FieldDescription, FieldLabel, FieldSeparator } from '@library/components/ui/field'
import { Input } from '@library/components/ui/input'
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from '@library/components/ui/input-otp'

const props = withDefaults(
  defineProps<{
    /** The address the code was sent to. */
    email: string
    /** Digits in the emailed code; the slots split into two halves. */
    codeLength?: number
    /** Under the code, e.g. when the code can be sent again. */
    codeHint?: string
    /** Shown as the card's title. */
    title?: string
    /** Shown under the title. */
    description?: string
    /** Shown above the email field. */
    emailLabel?: string
    /** Shown on the button that lets the shopper change the email address. */
    changeLabel?: string
    /** Shown above the code slots. */
    codeLabel?: string
    /** Shown on the button that submits the code. */
    submitLabel?: string
    /** Shown on the divider above the other sign-in options. */
    separatorLabel?: string
    /** Shown on the button that signs in with a passkey. */
    passkeyLabel?: string
    /** Shown on the button that switches to signing in with a password. */
    passwordLabel?: string
    /** Bare drops the edge, shadow and inset, for a section placed inside another Card. */
    surface?: 'raised' | 'bare'
    class?: HTMLAttributes['class']
  }>(),
  {
    surface: 'raised',
    codeLength: 6,
    title: 'Sign in',
    description: 'No password needed. We email you a code.',
    emailLabel: 'Email',
    changeLabel: 'Change',
    codeLabel: 'Verification code',
    submitLabel: 'Verify and sign in',
    separatorLabel: 'or',
    passkeyLabel: 'Use a passkey',
    passwordLabel: 'Sign in with password',
  },
)
const emit = defineEmits<{
  /** Fires when the customer asks to change the email address the code was sent to. */
  change: []
  /** Fires with the entered code when the customer completes or submits it. */
  submit: [code: string]
  /** Fires when the customer chooses to sign in with a passkey. */
  passkey: []
  /** Fires when the customer chooses to sign in with a password instead. */
  password: []
}>()

/** The code entered so far; submitted once all its digits are filled. */
const code = defineModel<string>('code', { default: '' })

const id = useId()
const half = computed(() => Math.ceil(props.codeLength / 2))
const complete = computed(() => code.value.length === props.codeLength)
</script>

<template>
  <Card :surface="surface" :class="props.class">
    <CardHeader>
      <CardTitle>{{ title }}</CardTitle>
      <CardDescription v-if="description">{{ description }}</CardDescription>
    </CardHeader>
    <CardContent>
      <form class="flex flex-col gap-6" @submit.prevent="complete && emit('submit', code)">
        <Field>
          <FieldLabel :for="`${id}-email`">{{ emailLabel }}</FieldLabel>
          <div class="flex gap-2">
            <Input :id="`${id}-email`" type="email" :model-value="email" readonly />
            <Button type="button" variant="outline" color="neutral" @click="emit('change')">{{ changeLabel }}</Button>
          </div>
        </Field>
        <Field>
          <FieldLabel>{{ codeLabel }}</FieldLabel>
          <InputOTP v-model="code" :maxlength="codeLength" @complete="emit('submit', $event)">
            <InputOTPGroup class="min-w-0">
              <InputOTPSlot v-for="index in half" :key="index" :index="index - 1" />
            </InputOTPGroup>
            <InputOTPSeparator />
            <InputOTPGroup class="min-w-0">
              <InputOTPSlot v-for="index in codeLength - half" :key="index" :index="half + index - 1" />
            </InputOTPGroup>
          </InputOTP>
          <FieldDescription v-if="codeHint">{{ codeHint }}</FieldDescription>
        </Field>
        <Button type="submit" class="w-full" :disabled="!complete">{{ submitLabel }}</Button>
        <FieldSeparator>{{ separatorLabel }}</FieldSeparator>
        <div class="flex flex-col gap-2">
          <Button type="button" variant="outline" color="neutral" class="w-full" @click="emit('passkey')">
            <KeyRoundIcon data-icon />
            {{ passkeyLabel }}
          </Button>
          <Button type="button" variant="ghost" color="neutral" class="w-full" @click="emit('password')">
            <LockIcon data-icon />
            {{ passwordLabel }}
          </Button>
        </div>
      </form>
    </CardContent>
  </Card>
</template>
```
