Skip to content
SLICES / TECHNICAL FOLIO Nº 001FULL-STACK ASSEMBLIES — OPEN SOURCE

Headless / Account

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.

24 public parts · 3examples

@slices.sh/auth/headless/passkeysView as Markdown ↗

Your design. Our behavior.

Explore the tree to see what each part controls. Gates reveal the relevant state; roots outline their scope.

Behavior / inputGateGenericRenderer · not headlessScope
COMPONENT ANATOMYHover · focus · tap
ONE POSSIBLE DESIGNStatic preview

User.Root · Scope

User.RootSays 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.

Installation

Choose what your component needs. Run these commands in an initialized Next.js app.

0

Before you start

The steps below run in a Next.js app with shadcn set up (it has a components.json). In an existing Next.js app, such as one from create-next-app, set shadcn up first:

npx shadcn@latest init -t next -b base -p nova

Or start fresh: the slices CLI creates the app and sets it up.

npx slices.sh install
1

Install the service registry

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

npx add services
2

Install the headless library

npm

Add the React parts to your app, then compose them with your own elements.

npm install @slices.sh/auth@^0.1.10

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

3

Install the service

Connect the parts to a provider. Skip this step if this service is already configured in your app.

Provider
Database
Authentication methods
Account capabilities
npx add auth.betterauth --passkeys

Adds service configuration and API routes to your app, with the provider dependencies. For a headless composition, add import "@/services" after "use client".

The slices CLI installs the service and provisions its required resources in one command.

Set BETTER_AUTH_URL to your app origin. Memory is for local development; use a persistent database for deployment. Neon and Supabase also require the migration script printed by the installer.

Provider setup notes
  • BETTER_AUTH_SECRET signs every session and is generated into .env.local while this installs (32 random bytes, base64, nothing leaves the machine). Changing it signs everyone out; in production set the same one on every instance.
  • The public origin (OAuth callbacks, the passkey domain) is the one each request comes to in development, and the deployment's own on Vercel. Elsewhere, or behind a proxy or a custom domain, set BETTER_AUTH_URL, such as https://acme.com.

Start small

Parts bring the behavior. Use their default elements, or supply your own with render.

example.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).

API reference

Every public part used by this experience is listed below. Gates render children without adding an element. Element parts accept native element props and render; function children receive the state declared in their props. Scopes provide context rather than a visual element.

Passkeys.Availablegate

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

Capability: methods: passkey

Props

1 documented props

Passkeys.Available props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.Rootscope

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

Capability: methods: passkey

Props

3 documented props

Passkeys.Root props
PropTypeDefault
config

The auth config, a slug from the registry (services/auth/<config>.config.ts). The nearest root's, else "default", when left out.

string—
enabled

Left out or true: required, and missing auth throws. "optional": renders its children and every part inside degrades without auth. false: off. A capability name such as "methods: password": requires auth with it.

Enabled—
children

Content rendered inside this part.

ReactNode—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.Formaction · <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.

Props

2 documented props · Native <form> attributes supported

Passkeys.Form props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.NameInputinput · <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.

Required scope

Passkeys.NameInput needs Passkeys.Form.

Props

2 documented props · Native <input> attributes supported

Passkeys.NameInput props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.NameLabelcontent · <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.

Required scope

Passkeys.NameLabel needs Passkeys.Form.

Props

2 documented props · Native <label> attributes supported

Passkeys.NameLabel props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.Addaction · <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.

Props

2 documented props · Native <button> attributes supported

Passkeys.Add props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { pending: boolean }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.Itemcontent · <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.

Required scope

Passkeys.Item provides its own scope.

Props

2 documented props · Native <li> attributes supported

Passkeys.Item props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { name: string | null; createdAt: Date }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.RequestRenameaction · <button>

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

Required scope

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

Props

2 documented props · Native <button> attributes supported

Passkeys.RequestRename props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.RenameFormaction · <form>

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

Required scope

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

Props

2 documented props · Native <form> attributes supported

Passkeys.RenameForm props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.RenameInputinput · <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.

Required scope

Passkeys.RenameInput needs Passkeys.RenameForm.

Props

2 documented props · Native <input> attributes supported

Passkeys.RenameInput props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.Renameaction · <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.

Required scope

Passkeys.Rename needs Passkeys.RenameForm.

Props

2 documented props · Native <button> attributes supported

Passkeys.Rename props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { pending: boolean }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.CancelRenameaction · <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.

Required scope

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

Props

2 documented props · Native <button> attributes supported

Passkeys.CancelRename props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.RequestRemoveaction · <button>

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

Capability: manage-passkeys

Required scope

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

Props

2 documented props · Native <button> attributes supported

Passkeys.RequestRemove props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.ConfirmRemovegate

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.

Capability: manage-passkeys

Required scope

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

Props

1 documented props

Passkeys.ConfirmRemove props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.CancelRemoveaction · <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.

Capability: manage-passkeys

Required scope

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

Props

2 documented props · Native <button> attributes supported

Passkeys.CancelRemove props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.Removeaction · <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.

Capability: manage-passkeys

Required scope

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

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

Props

2 documented props · Native <button> attributes supported

Passkeys.Remove props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { pending: boolean }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.Removedcontent · <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.

Capability: manage-passkeys

Required scope

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

Props

2 documented props · Native <p> attributes supported

Passkeys.Removed props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.Emptycontent · <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.

Props

2 documented props · Native <p> attributes supported

Passkeys.Empty props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.Loadingcontent · <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.

Props

2 documented props · Native <p> attributes supported

Passkeys.Loading props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.Errorcontent · <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.

Props

2 documented props · Native <div> attributes supported

Passkeys.Error props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { message: string }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.SetupPromptgate

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.

Props

1 documented props

Passkeys.SetupPrompt props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.Setupaction · <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.

Props

2 documented props · Native <button> attributes supported

Passkeys.Setup props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { pending: boolean }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

Passkeys.Dismissaction · <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.

Props

2 documented props · Native <button> attributes supported

Passkeys.Dismiss props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

User.AddPasskeyaction · <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.

Capability: methods: passkey

Props

2 documented props · Native <button> attributes supported

User.AddPasskey props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { pending: boolean }) => ReactNode)—
render

Replace the default element with your own while retaining the part’s behavior and accessibility.

ReactElement—

Required props are marked. A dash means no explicit default is declared in this component.

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

User.Rootscope

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.

Props

3 documented props

User.Root props
PropTypeDefault
config

The auth config, a slug from the registry (services/auth/<config>.config.ts). The nearest root's, else "default", when left out.

string—
enabled

Left out or true: required, and missing auth throws. "optional": renders its children and every part inside degrades without auth. false: off. A capability name such as "methods: password": requires auth with it.

Enabled—
children

Content rendered inside this part.

ReactNode—

Required props are marked. A dash means no explicit default is declared in this component.

Alternate public imports
  • User.Root from @slices.sh/auth/headless/user
  • User.Root from @slices.sh/auth/headless/passkeys
  • User.Root from @slices.sh/auth/headless/two-factor

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

When something is missing

No service configuration
Install the auth setup below and import "@/services". Required service roots report missing setup; gates do not replace that configuration.
Missing capability or parent scope
Follow each part’s context requirements and the example composition. Capability-aware controls hide when the provider cannot perform their action.
Read the setup guide →