# Login Passkey

> A passwordless sign-in screen that runs a simulated face scan in a dialog, then welcomes the user.

A shadcn/ui block from the [@esuiss](https://suiss.app/ui?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/esuiss/login-passkey).

- Page, with a live preview: https://shadcn.exchange/r/esuiss/login-passkey
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/esuiss/login-passkey-light-8da46367f2c1e7a7.png
- Registry item: https://suiss.app/ui/r/new-york-v4/login-passkey.json
- Categories: blocks, login
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/esuiss/login-passkey.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@esuiss/avatar`, `@esuiss/button`, `@esuiss/dialog`

## Files

### `bases/radix/components/blocks/login-passkey.tsx`

```tsx
"use client"

import * as React from "react"

import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogTitle,
} from "@/components/ui/dialog"

type Status = "idle" | "scan" | "ok" | "done"

const avatarClass =
  "bg-linear-to-br from-[oklch(0.78_0.1_250)] to-[oklch(0.6_0.15_280)] text-white dark:from-[oklch(0.78_0.1_250)] dark:to-[oklch(0.6_0.15_280)]"

function ScanGlyph({ verified }: { verified: boolean }) {
  return (
    <div className="relative size-18">
      <IconPlaceholder
        lucide="ScanFaceIcon"
        tabler="IconFaceId"
        hugeicons="FaceIdIcon"
        phosphor="ScanSmileyIcon"
        remixicon="RiUserSmileLine"
        data-verified={verified ? "" : undefined}
        className="absolute inset-0 size-full animate-[pulse_1.2s_ease-in-out_infinite] text-foreground transition-[opacity,scale] duration-500 data-verified:scale-60 data-verified:animate-none data-verified:opacity-0 motion-reduce:animate-none"
      />
      <span
        data-verified={verified ? "" : undefined}
        className="absolute inset-1.5 flex scale-40 items-center justify-center rounded-full bg-green-500 text-white opacity-0 transition-[opacity,scale] duration-[300ms,550ms] ease-[ease,cubic-bezier(0.3,1.25,0.5,1)] data-verified:scale-100 data-verified:opacity-100"
      >
        <IconPlaceholder
          lucide="CheckIcon"
          tabler="IconCheck"
          hugeicons="Tick02Icon"
          phosphor="CheckIcon"
          remixicon="RiCheckLine"
          className="size-8"
        />
      </span>
    </div>
  )
}

export function LoginPasskey() {
  const [status, setStatus] = React.useState<Status>("idle")
  const timers = React.useRef<number[]>([])

  const clear = () => {
    for (const timer of timers.current) window.clearTimeout(timer)
    timers.current = []
  }

  React.useEffect(
    () => () => {
      for (const timer of timers.current) window.clearTimeout(timer)
    },
    []
  )

  const start = () => {
    setStatus("scan")
    timers.current.push(
      window.setTimeout(() => setStatus("ok"), 1500),
      window.setTimeout(() => setStatus("done"), 2400)
    )
  }

  const cancel = () => {
    clear()
    setStatus("idle")
  }

  if (status === "done") {
    return (
      <section className="flex min-h-150 items-center justify-center p-6">
        <div className="flex flex-col items-center gap-3.5 text-center transition-[opacity,translate] duration-1000 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-4.5 starting:opacity-0 motion-reduce:transition-none">
          <Avatar className="size-21">
            <AvatarFallback className={`${avatarClass} text-3xl`}>
              JR
            </AvatarFallback>
          </Avatar>
          <h1 className="text-4xl font-semibold tracking-tight md:text-5xl">
            Welcome, Jamie.
          </h1>
          <p className="text-lg text-muted-foreground">
            You signed in with a passkey. No password needed.
          </p>
          <button
            type="button"
            onClick={() => setStatus("idle")}
            className="rounded-xs text-lg text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
          >
            Sign out ›
          </button>
        </div>
      </section>
    )
  }

  return (
    <section className="flex min-h-150 items-center justify-center p-6">
      <div className="flex max-w-140 flex-col items-center text-center">
        <span className="flex size-18 items-center justify-center rounded-full bg-muted text-primary">
          <IconPlaceholder
            lucide="KeyIcon"
            tabler="IconKey"
            hugeicons="Key01Icon"
            phosphor="KeyIcon"
            remixicon="RiKey2Line"
            className="size-8.5"
          />
        </span>
        <h1 className="mt-5.5 text-4xl font-semibold tracking-tight text-balance md:text-5xl">
          Sign in without a password.
        </h1>
        <p className="mt-3 max-w-120 text-xl text-balance text-muted-foreground">
          With a passkey, your face or fingerprint is all it takes. Nothing to
          remember, nothing to type.
        </p>
        <div className="mt-7 flex items-center gap-2.5 rounded-full bg-muted py-2 ps-2 pe-4">
          <Avatar size="sm" className="size-7">
            <AvatarFallback className={`${avatarClass} text-xs`}>
              JR
            </AvatarFallback>
          </Avatar>
          <span className="text-base">jamie@example.com</span>
          <a href="#change" className="text-sm text-primary hover:underline">
            Change ›
          </a>
        </div>
        <Button size="lg" onClick={start} className="mt-5.5 gap-2">
          <IconPlaceholder
            lucide="ScanFaceIcon"
            tabler="IconFaceId"
            hugeicons="FaceIdIcon"
            phosphor="ScanSmileyIcon"
            remixicon="RiUserSmileLine"
            className="size-4.5"
          />
          Continue with passkey
        </Button>
        <a
          href="#password"
          className="mt-4.5 text-sm text-primary hover:underline"
        >
          Sign in with password ›
        </a>
        <p className="mt-10 max-w-105 text-xs leading-normal text-muted-foreground">
          Passkeys are end-to-end encrypted and sync across all your devices
          with suiss Keychain.
        </p>
      </div>
      <Dialog
        open={status === "scan" || status === "ok"}
        onOpenChange={(open) => {
          if (!open) cancel()
        }}
      >
        <DialogContent
          showCloseButton={false}
          className="flex w-65 flex-col items-center gap-3.5 rounded-3xl bg-popover/82 px-6 pt-7.5 pb-4.5 text-center backdrop-blur-2xl backdrop-saturate-180 data-open:zoom-in-90 sm:max-w-65"
        >
          <ScanGlyph verified={status === "ok"} />
          <div className="flex flex-col gap-0.75">
            <DialogTitle className="text-lg font-semibold">
              {status === "ok" ? "Verified" : "Face scan"}
            </DialogTitle>
            <DialogDescription className="text-sm text-muted-foreground">
              Passkey for suiss.com
            </DialogDescription>
          </div>
          <DialogClose asChild>
            <Button
              variant="plain"
              data-hidden={status === "ok" ? "" : undefined}
              className="text-base data-hidden:invisible"
            >
              Cancel
            </Button>
          </DialogClose>
        </DialogContent>
      </Dialog>
    </section>
  )
}
```
