# Passkeys

Signing in with a passkey, the list of a person's passkeys, adding, naming and removing one, and the offer made to someone who tried a passkey and had none. A subpath of its own, installed only where an app offers passkeys. It exports SignIn and User as the auth namespaces with the passkey parts added, so a screen keeps writing SignIn.Passkey and User.AddPasskey, and a Passkeys namespace for the manager.

Import: @slices.sh/auth/headless/passkeys

Public parts: 24

## Installation

### 1. Install the service registry

services/index.ts, which `import "@/services"` loads. A provider setup writes the same file; skip this if your app has one.

```bash
npx shadcn@latest add "https://slices.sh/r/services.json"
```

### 2. Install the headless library

```bash
npm install @slices.sh/auth@^0.1.10
```

### 3. Install the service

Install the provider configuration via shadcn or the slices CLI. Skip this step if your app already has the service configured. Provider commands and provisioning: https://slices.sh/docs/services/auth.md

## Anatomy

### Composition

```text
User.Root
  Passkeys.Root
    Passkeys.Available
      Passkeys.Item
        Passkeys.ConfirmRemove
          Passkeys.Remove
          Passkeys.CancelRemove
        Passkeys.RequestRename
        Passkeys.RequestRemove
        Passkeys.RenameForm
          Passkeys.RenameInput
          Passkeys.Rename
          Passkeys.CancelRename
      Passkeys.Form
        Passkeys.NameLabel
        Passkeys.NameInput
        Passkeys.Add
        User.AddPasskey
    Passkeys.Removed
    Passkeys.SetupPrompt
      Passkeys.Setup
      Passkeys.Dismiss
    Passkeys.Loading
    Passkeys.Empty
    Passkeys.Error
```

## Register and manage passkeys

```tsx
"use client"

import "@/services"
import { Passkeys } from "@slices.sh/auth/headless/passkeys"

export function Example() {
  return (
    <Passkeys.Root>
      <Passkeys.Form>
        <Passkeys.NameLabel>Passkey name</Passkeys.NameLabel>
        <Passkeys.NameInput />
        <Passkeys.Add>Add a passkey</Passkeys.Add>
      </Passkeys.Form>
      <ul>
        <Passkeys.Item>
          <Passkeys.RequestRename>Rename</Passkeys.RequestRename>
          <Passkeys.RenameForm>
            <Passkeys.RenameInput />
            <Passkeys.Rename>Save name</Passkeys.Rename>
            <Passkeys.CancelRename>Cancel</Passkeys.CancelRename>
          </Passkeys.RenameForm>
          <Passkeys.RequestRemove>Remove</Passkeys.RequestRemove>
          <Passkeys.ConfirmRemove>
            <Passkeys.Remove>Confirm removal</Passkeys.Remove>
            <Passkeys.CancelRemove>Cancel</Passkeys.CancelRemove>
          </Passkeys.ConfirmRemove>
        </Passkeys.Item>
      </ul>
      <Passkeys.Empty>No passkeys yet.</Passkeys.Empty>
      <Passkeys.Error />
    </Passkeys.Root>
  )
}

```

Enable passkeys. Use an authenticated account and a secure browser context (HTTPS or localhost).

## Sign in with a passkey

```tsx
"use client"

// services/index.ts comes from https://slices.sh/r/services.json (or any provider setup)
import "@/services"
import { SignIn } from "@slices.sh/auth/headless/passkeys"

export function Example() {
  return (
    <SignIn.Root>
      <SignIn.Passkey>Use a passkey</SignIn.Passkey>
      <SignIn.Error />
    </SignIn.Root>
  )
}

```

Enable passkeys in the auth provider. This signs in an account that already registered a passkey.

## Offer enrollment

```tsx
"use client"

// services/index.ts comes from https://slices.sh/r/services.json (or any provider setup)
import "@/services"
import { Passkeys, User } from "@slices.sh/auth/headless/passkeys"

export function Example() {
  return (
    <Passkeys.Root>
      <User.SignedIn>
        <User.AddPasskey>Add a passkey</User.AddPasskey>
      </User.SignedIn>
    </Passkeys.Root>
  )
}

```

A signed-in user can add a passkey through the browser's native prompt.

## Complete API reference

### Passkeys.Available

gate

Available is a gate for anything outside the manager that should appear only where passkeys work, such as a settings entry that opens it.

Import: @slices.sh/auth/headless/passkeys

Capability: methods: passkey

```tsx
GateProps
```

Source: @slices.sh/auth/src/headless/passkeys/available.tsx

### Passkeys.Root

scope

Available is a gate for anything outside the manager that should appear only where passkeys work, such as a settings entry that opens it.

Import: @slices.sh/auth/headless/passkeys

Capability: methods: passkey

```tsx
RootProps & AuthRootProps
```

Source: @slices.sh/auth/src/headless/passkeys/root.tsx

### Passkeys.Form

action · Element: form

Form holds the optional name and runs the ceremony on submit. NameLabel locks htmlFor to NameInput, an <input autocomplete="off" maxlength="80">. Add is the form's one submit, with { pending } while the browser asks.

Import: @slices.sh/auth/headless/passkeys

```tsx
PartProps<"form">
```

Source: @slices.sh/auth/src/headless/passkeys/form.tsx

### Passkeys.NameInput

input · Element: input

Form holds the optional name and runs the ceremony on submit. NameLabel locks htmlFor to NameInput, an <input autocomplete="off" maxlength="80">. Add is the form's one submit, with { pending } while the browser asks.

Import: @slices.sh/auth/headless/passkeys

Passkeys.NameInput needs Passkeys.Form.

```tsx
PartProps<"input">
```

Source: @slices.sh/auth/src/headless/passkeys/name-input.tsx

### Passkeys.NameLabel

content · Element: label

Form holds the optional name and runs the ceremony on submit. NameLabel locks htmlFor to NameInput, an <input autocomplete="off" maxlength="80">. Add is the form's one submit, with { pending } while the browser asks.

Import: @slices.sh/auth/headless/passkeys

Passkeys.NameLabel needs Passkeys.Form.

```tsx
PartProps<"label">
```

Source: @slices.sh/auth/src/headless/passkeys/name-label.tsx

### Passkeys.Add

action · Element: button

Form holds the optional name and runs the ceremony on submit. NameLabel locks htmlFor to NameInput, an <input autocomplete="off" maxlength="80">. Add is the form's one submit, with { pending } while the browser asks.

Import: @slices.sh/auth/headless/passkeys

```tsx
PartProps<"button", { pending: boolean }>
```

Source: @slices.sh/auth/src/headless/passkeys/add.tsx

### Passkeys.Item

content · Element: li

Item is an <li> repeated once per credential, with { name, createdAt }; name is null for a credential never named. Loading and Empty are <p role="status">, and Empty appears only once the list has answered, so a list that failed claims nothing. Error is <div role="alert"> with { message }, for loading, adding, renaming or removing.

Import: @slices.sh/auth/headless/passkeys

Passkeys.Item provides its own scope.

```tsx
Props

type Props = PartProps<"li", { name: string | null; createdAt: Date }>
```

Source: @slices.sh/auth/src/headless/passkeys/item.tsx

### Passkeys.RequestRename

action · Element: button

Opens the name of this credential for editing. Absent where the backend cannot rename one.

Import: @slices.sh/auth/headless/passkeys

Passkeys.RequestRename must be inside <Passkeys.Item>.

```tsx
PartProps<"button">
```

Source: @slices.sh/auth/src/headless/passkeys/request-rename.tsx

### Passkeys.RenameForm

action · Element: form

The name of one credential, open for editing. Nothing on a row nobody asked to rename.

Import: @slices.sh/auth/headless/passkeys

Passkeys.RenameForm must be inside <Passkeys.Item>.

```tsx
PartProps<"form">
```

Source: @slices.sh/auth/src/headless/passkeys/rename-form.tsx

### Passkeys.RenameInput

input · Element: input

Inside an Item. RequestRename opens that row's name for editing and stands aside while it is open. RenameForm renders only while open, seeded with the current name. RenameInput takes the focus the button dropped. Rename submits, disabled until the name is more than spaces. CancelRename closes the editor and drops the last refusal.

Import: @slices.sh/auth/headless/passkeys

Passkeys.RenameInput needs Passkeys.RenameForm.

```tsx
PartProps<"input">
```

Source: @slices.sh/auth/src/headless/passkeys/rename-input.tsx

### Passkeys.Rename

action · Element: button

Inside an Item. RequestRename opens that row's name for editing and stands aside while it is open. RenameForm renders only while open, seeded with the current name. RenameInput takes the focus the button dropped. Rename submits, disabled until the name is more than spaces. CancelRename closes the editor and drops the last refusal.

Import: @slices.sh/auth/headless/passkeys

Passkeys.Rename needs Passkeys.RenameForm.

```tsx
PartProps<"button", { pending: boolean }>
```

Source: @slices.sh/auth/src/headless/passkeys/rename.tsx

### Passkeys.CancelRename

action · Element: button

Inside an Item. RequestRename opens that row's name for editing and stands aside while it is open. RenameForm renders only while open, seeded with the current name. RenameInput takes the focus the button dropped. Rename submits, disabled until the name is more than spaces. CancelRename closes the editor and drops the last refusal.

Import: @slices.sh/auth/headless/passkeys

Passkeys.CancelRename must be inside <Passkeys.Item>.

```tsx
PartProps<"button">
```

Source: @slices.sh/auth/src/headless/passkeys/cancel-rename.tsx

### Passkeys.RequestRemove

action · Element: button

Asks to forget this credential. Absent where the backend cannot forget one.

Import: @slices.sh/auth/headless/passkeys

Capability: manage-passkeys

Passkeys.RequestRemove must be inside <Passkeys.Item>.

```tsx
PartProps<"button">
```

Source: @slices.sh/auth/src/headless/passkeys/request-remove.tsx

### Passkeys.ConfirmRemove

gate

Inside an Item. RequestRemove arms the confirmation and renders nothing where the backend cannot forget a credential. ConfirmRemove is the gate for the armed question; Remove does it with { pending }, and CancelRemove takes the focus back. Removed is <p role="status"> for a removal this browser could not be told about: the entry is still in the password manager and only the person can delete it.

Import: @slices.sh/auth/headless/passkeys

Capability: manage-passkeys

Passkeys.ConfirmRemove must be inside <Passkeys.Item>.

```tsx
GateProps
```

Source: @slices.sh/auth/src/headless/passkeys/confirm-remove.tsx

### Passkeys.CancelRemove

action · Element: button

Inside an Item. RequestRemove arms the confirmation and renders nothing where the backend cannot forget a credential. ConfirmRemove is the gate for the armed question; Remove does it with { pending }, and CancelRemove takes the focus back. Removed is <p role="status"> for a removal this browser could not be told about: the entry is still in the password manager and only the person can delete it.

Import: @slices.sh/auth/headless/passkeys

Capability: manage-passkeys

Passkeys.CancelRemove must be inside <Passkeys.Item>.

```tsx
PartProps<"button">
```

Source: @slices.sh/auth/src/headless/passkeys/cancel-remove.tsx

### Passkeys.Remove

action · Element: button

Inside an Item. RequestRemove arms the confirmation and renders nothing where the backend cannot forget a credential. ConfirmRemove is the gate for the armed question; Remove does it with { pending }, and CancelRemove takes the focus back. Removed is <p role="status"> for a removal this browser could not be told about: the entry is still in the password manager and only the person can delete it.

Import: @slices.sh/auth/headless/passkeys

Capability: manage-passkeys

Passkeys.Remove must be inside <Passkeys.Item>.

Passkeys.Remove must be inside <Passkeys.Root>.

```tsx
PartProps<"button", { pending: boolean }>
```

Source: @slices.sh/auth/src/headless/passkeys/remove.tsx

### Passkeys.Removed

content · Element: p

Inside an Item. RequestRemove arms the confirmation and renders nothing where the backend cannot forget a credential. ConfirmRemove is the gate for the armed question; Remove does it with { pending }, and CancelRemove takes the focus back. Removed is <p role="status"> for a removal this browser could not be told about: the entry is still in the password manager and only the person can delete it.

Import: @slices.sh/auth/headless/passkeys

Capability: manage-passkeys

Passkeys.Removed must be inside <Passkeys.Root>.

```tsx
PartProps<"p">
```

Source: @slices.sh/auth/src/headless/passkeys/removed.tsx

### Passkeys.Empty

content · Element: p

Item is an <li> repeated once per credential, with { name, createdAt }; name is null for a credential never named. Loading and Empty are <p role="status">, and Empty appears only once the list has answered, so a list that failed claims nothing. Error is <div role="alert"> with { message }, for loading, adding, renaming or removing.

Import: @slices.sh/auth/headless/passkeys

```tsx
PartProps<"p">
```

Source: @slices.sh/auth/src/headless/passkeys/empty.tsx

### Passkeys.Loading

content · Element: p

Item is an <li> repeated once per credential, with { name, createdAt }; name is null for a credential never named. Loading and Empty are <p role="status">, and Empty appears only once the list has answered, so a list that failed claims nothing. Error is <div role="alert"> with { message }, for loading, adding, renaming or removing.

Import: @slices.sh/auth/headless/passkeys

```tsx
PartProps<"p">
```

Source: @slices.sh/auth/src/headless/passkeys/loading.tsx

### Passkeys.Error

content · Element: div

Item is an <li> repeated once per credential, with { name, createdAt }; name is null for a credential never named. Loading and Empty are <p role="status">, and Empty appears only once the list has answered, so a list that failed claims nothing. Error is <div role="alert"> with { message }, for loading, adding, renaming or removing.

Import: @slices.sh/auth/headless/passkeys

```tsx
PartProps<"div", { message: string }>
```

Source: @slices.sh/auth/src/headless/passkeys/error.tsx

### Passkeys.SetupPrompt

gate

SetupPrompt opens when someone asked for a passkey at sign-in, this browser has them, and the account has none. Setup runs the ceremony from a click, never an effect, since Safari and Firefox refuse it without a fresh gesture. Dismiss turns the offer down for the session.

Import: @slices.sh/auth/headless/passkeys

```tsx
GateProps
```

Source: @slices.sh/auth/src/headless/passkeys/setup-prompt.tsx

### Passkeys.Setup

action · Element: button

SetupPrompt opens when someone asked for a passkey at sign-in, this browser has them, and the account has none. Setup runs the ceremony from a click, never an effect, since Safari and Firefox refuse it without a fresh gesture. Dismiss turns the offer down for the session.

Import: @slices.sh/auth/headless/passkeys

```tsx
PartProps<"button", { pending: boolean }>
```

Source: @slices.sh/auth/src/headless/passkeys/setup.tsx

### Passkeys.Dismiss

action · Element: button

SetupPrompt opens when someone asked for a passkey at sign-in, this browser has them, and the account has none. Setup runs the ceremony from a click, never an effect, since Safari and Firefox refuse it without a fresh gesture. Dismiss turns the offer down for the session.

Import: @slices.sh/auth/headless/passkeys

```tsx
PartProps<"button">
```

Source: @slices.sh/auth/src/headless/passkeys/dismiss.tsx

### User.AddPasskey

action · Element: button

AddPasskey is <button type="button"> with { pending }: one click registers a passkey for the signed-in person, for a menu or a banner that has no room for the manager. It renders nothing without passkey support.

Import: @slices.sh/auth/headless/passkeys

Capability: methods: passkey

```tsx
PartProps<"button", { pending: boolean }>
```

Source: @slices.sh/auth/src/headless/passkeys/user/add-passkey.tsx

### User.Root

scope

Says which auth the User parts below read, and whether they may do without it. The parts need
no root: left out, they read the nearest auth root's config, or "default", and render nothing
without auth. `<User.Root enabled="optional">` is for the one who wants that said in the code.

Import: @slices.sh/auth/headless

```tsx
RootProps & AuthRootProps
```

Alternate import: User.Root from @slices.sh/auth/headless/user

Alternate import: User.Root from @slices.sh/auth/headless/passkeys

Alternate import: User.Root from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/root.tsx
