Skip to content

Login & Passkey

The gate to the treasury. A clean sign-in — email, password, remember-me — with a StatefulButton that carries the request from submitting to signed-in, a form-level banner when the harbourmaster turns you away, and a passkey path for crews who have already left a key on file. Every state is forced through props here; in product the form drives itself.

Login form

One controlled/uncontrolled form, four forced states. Field errors ride on the Input (message + shake); a form-level failure raises the aria-live banner and the submit button rolls to its error face.

idle

Sign in

Welcome back to your treasury.

Forgot password?

Empty and interactive — type to try the show/hide toggle and remember-me.

error (forced)

Sign in

Welcome back to your treasury.

Forgot password?

status='error' + a controlled errors map — both fields flag and shake, the banner announces, the button says Try again.

submitting

Sign in

Welcome back to your treasury.

Forgot password?

Inputs lock; the submit button spins on its loading text.

success

Sign in

Welcome back to your treasury.

Forgot password?

The button lands on its success check — the session is authenticated.

import { LoginForm } from "@coldharbor/ui";

<LoginForm
  onSubmit={async (values) => signIn(values)}
  status={status}   // "idle" | "submitting" | "error" | "success"
  errors={errors}   // force per-field messages
  onChange={setValues}
/>
  • --ch-surface
  • --ch-border
  • --ch-fg
  • --ch-fg-muted
  • --ch-primary
  • --ch-danger
  • --ch-focus

With passkey

Pass `passkey` to append the 'or' divider and an embedded PasskeyButton wired to `onPasskey`. The footer slot takes any node — here, a route back to onboarding.

passkey enabled

Sign in

Welcome back to your treasury.

Forgot password?
New to ColdHarbor? Request an invite

The passkey button sits below the divider at the submit's size; it runs its own authenticating → success cycle from onPasskey.

<LoginForm
  passkey
  onPasskey={() => navigator.credentials.get({ publicKey })}
  footer={<a href="/signup">Create an account</a>}
/>
  • --ch-surface
  • --ch-border
  • --ch-fg
  • --ch-fg-muted
  • --ch-primary
  • --ch-danger
  • --ch-focus
  • --ch-accent

Passkey button states

The biometric button in every state. authenticating sweeps an ion scan line and pulse ring over the fingerprint; success draws a check in success tone; error flips danger and shakes once.

idle

Aurum fingerprint on the outlined surface — the resting biometric prompt.

authenticating

Scan sweep + pulse ring; the button is aria-busy and disabled while it works.

success

Check in the success tone — the credential was verified.

error

Alert icon, danger tone, one shake — the passkey was rejected.

import { PasskeyButton } from "@coldharbor/ui";

// Uncontrolled — the promise drives idle → authenticating → success/error.
<PasskeyButton onActivate={() => authenticatePasskey()} />

// Or force any state.
<PasskeyButton status="authenticating" />
  • --ch-accent
  • --ch-primary
  • --ch-success
  • --ch-danger
  • --ch-border-strong
  • --ch-focus

Registered passkeys

PasskeyChip is one row of the security screen's credential list — an aurum key seal, the device name, an optional 'This device' tag, and the mono creation date.

credential list
iPhone 15 ProThis deviceAdded 12 Aug 2026
MacBook Pro (Touch ID)Added 4 Jul 2026
YubiKey 5C NFCAdded 21 Mar 2026
import { PasskeyChip } from "@coldharbor/ui";

<PasskeyChip device="iPhone 15 Pro" created="12 Aug 2026" current />
<PasskeyChip device="YubiKey 5C NFC" created="21 Mar 2026" />
  • --ch-surface
  • --ch-border
  • --ch-accent
  • --ch-fg
  • --ch-fg-faint
  • --ch-font-family-mono