Settings
Account settings screens — a grouped account form, a notification-preferences matrix and a connected-apps integrations list.
Preview
@props([
'heading' => 'Passkeys',
'subheading' => 'Sign in with your fingerprint, face or device PIN instead of a password.',
'headingTag' => 'h2',
// [['id' => …, 'name' => 'MacBook', 'createdAt' => DateTimeInterface|string,
// 'lastUsedAt' => DateTimeInterface|string|null], …]. Null shows demo rows.
'passkeys' => null,
// WebAuthn registration endpoints (options, POST verify). Null: the
// preset routes, else /passkeys/options and /passkeys.
'optionsUrl' => null,
'verifyUrl' => null,
// DELETE target for one passkey; `{id}` is replaced by the passkey id.
// Null: /user/passkeys/{id} with the laravel-passkeys preset, else
// /passkeys/{id}.
'removeAction' => null,
// Backend contract, passed to the passkey item: 'laravel-passkeys' or null,
// and the single settings a preset makes (see the passkey item).
'preset' => null,
'optionsMethod' => null,
'optionsKey' => null,
'credentialKey' => null,
// Extra verify body fields.
'body' => [],
])
{{--
Settings Passkeys — the passkey list for a security settings page. Each row
shows the name, when it was created and last used, and a Remove action that
asks for confirmation first. "Add passkey" names the new passkey and runs
the WebAuthn registration through <x-ui.passkey>; the page reloads on
success so the list shows it.
--}}
@php
$headingTag = in_array($headingTag, ['h1', 'h2', 'h3'], true) ? $headingTag : 'h2';
$laravelPasskeys = $preset === 'laravel-passkeys';
$optionsUrl ??= $laravelPasskeys ? null : '/passkeys/options';
$verifyUrl ??= $laravelPasskeys ? null : '/passkeys';
$removeAction ??= $laravelPasskeys ? '/user/passkeys/{id}' : '/passkeys/{id}';
$passkeys ??= [
['id' => 'pk_1', 'name' => 'MacBook Pro', 'createdAt' => now()->subMonths(5), 'lastUsedAt' => now()->subHours(3)],
['id' => 'pk_2', 'name' => 'iPhone', 'createdAt' => now()->subMonths(2), 'lastUsedAt' => now()->subDays(9)],
['id' => 'pk_3', 'name' => 'YubiKey 5C', 'createdAt' => now()->subWeeks(1), 'lastUsedAt' => null],
];
$date = static function ($value): array {
if ($value instanceof \DateTimeInterface) {
$carbon = \Illuminate\Support\Carbon::instance($value);
return [$carbon->toIso8601String(), $carbon->translatedFormat('j M Y')];
}
return [null, (string) $value];
};
$uid = 'settings-passkeys-'.\Illuminate\Support\Str::random(6);
@endphp
<x-ui.section data-slot="block-settings-passkeys" aria-labelledby="{{ $uid }}-heading" space="md" {{ $attributes }}>
<div class="mx-auto flex max-w-3xl flex-col gap-6 px-4">
<div>
<{{ $headingTag }} id="{{ $uid }}-heading" class="text-2xl font-bold tracking-tight">{{ __($heading) }}</{{ $headingTag }}>
<p class="mt-2 text-sm text-muted-foreground">{{ __($subheading) }}</p>
</div>
<x-ui.card>
<x-ui.card.header>
<x-ui.card.title>{{ __('Your passkeys') }}</x-ui.card.title>
<x-ui.card.description>{{ __('Remove a passkey you no longer use or do not recognise.') }}</x-ui.card.description>
</x-ui.card.header>
<x-ui.card.content>
@if ($passkeys === [])
<p data-slot="settings-passkeys-empty" class="text-sm text-muted-foreground">{{ __('No passkeys yet. Add one below to sign in without a password.') }}</p>
@else
<ul role="list" class="divide-y divide-border">
@foreach ($passkeys as $passkey)
@php
$id = (string) ($passkey['id'] ?? $loop->index);
$name = (string) ($passkey['name'] ?? __('Passkey'));
[$createdIso, $createdText] = $date($passkey['createdAt'] ?? '');
$lastUsed = $passkey['lastUsedAt'] ?? null;
[$usedIso, $usedText] = $lastUsed === null ? [null, null] : $date($lastUsed);
$formId = $uid.'-remove-'.$loop->index;
@endphp
<li data-slot="settings-passkeys-item" class="flex flex-col gap-4 py-4 first:pt-0 last:pb-0 sm:flex-row sm:items-center">
<div class="flex min-w-0 flex-1 items-center gap-4">
<div class="grid size-10 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground" aria-hidden="true">
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="15" r="4" /><path d="m10.85 12.15 7.65-7.65M18 5l2 2M15 8l2 2" /></svg>
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-foreground">{{ $name }}</p>
<p class="flex flex-wrap gap-x-4 text-xs text-muted-foreground">
<span>{{ __('Created') }} @if ($createdIso)<time datetime="{{ $createdIso }}">{{ $createdText }}</time>@else{{ $createdText }}@endif</span>
<span>
@if ($lastUsed === null)
{{ __('Never used') }}
@else
{{ __('Last used') }} @if ($usedIso)<time datetime="{{ $usedIso }}">{{ $usedText }}</time>@else{{ $usedText }}@endif
@endif
</span>
</p>
</div>
</div>
<x-ui.form :id="$formId" :action="str_replace('{id}', rawurlencode($id), $removeAction)" method="DELETE" class="hidden" />
<x-ui.alert-dialog>
<x-ui.alert-dialog.trigger
aria-label="{{ __('Remove :name', ['name' => $name]) }}"
class="h-9 shrink-0 gap-2 self-start rounded-md px-4 text-sm font-medium text-destructive-text transition-colors hover:bg-destructive-soft focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring focus-visible:ring-offset-[length:var(--ring-offset-width)] focus-visible:ring-offset-background motion-reduce:transition-none sm:self-auto"
>
{{ __('Remove') }}
</x-ui.alert-dialog.trigger>
<x-ui.alert-dialog.content>
<x-ui.alert-dialog.header>
<x-ui.alert-dialog.title>{{ __('Remove the passkey “:name”?', ['name' => $name]) }}</x-ui.alert-dialog.title>
<x-ui.alert-dialog.description>{{ __('You can no longer sign in with it. Keep another passkey or your password so you do not lose access.') }}</x-ui.alert-dialog.description>
</x-ui.alert-dialog.header>
<x-ui.alert-dialog.footer>
<x-ui.alert-dialog.cancel>{{ __('Cancel') }}</x-ui.alert-dialog.cancel>
<x-ui.button type="submit" variant="destructive" :form="$formId">{{ __('Remove passkey') }}</x-ui.button>
</x-ui.alert-dialog.footer>
</x-ui.alert-dialog.content>
</x-ui.alert-dialog>
</li>
@endforeach
</ul>
@endif
</x-ui.card.content>
</x-ui.card>
<x-ui.card>
<x-ui.card.header>
<x-ui.card.title>{{ __('Add a passkey') }}</x-ui.card.title>
<x-ui.card.description>{{ __('Give it a name you will recognise, such as the device it lives on.') }}</x-ui.card.description>
</x-ui.card.header>
<x-ui.card.content>
<x-ui.passkey
mode="register" :options-url="$optionsUrl" :verify-url="$verifyUrl" :reload="true"
:preset="$preset" :options-method="$optionsMethod" :options-key="$optionsKey"
:credential-key="$credentialKey" :body="$body"
>
<form x-on:submit.prevent="start()" class="flex flex-col gap-4 sm:flex-row sm:items-end">
<x-ui.field class="min-w-0 flex-1">
<x-ui.label for="{{ $uid }}-name" required>{{ __('Passkey name') }}</x-ui.label>
<x-ui.input id="{{ $uid }}-name" x-model="name" required maxlength="64" autocomplete="off" :placeholder="__('Work laptop')" />
</x-ui.field>
<x-ui.passkey.button type="submit" class="sm:w-auto">{{ __('Add passkey') }}</x-ui.passkey.button>
</form>
<x-ui.passkey.status />
</x-ui.passkey>
</x-ui.card.content>
</x-ui.card>
</div>
</x-ui.section>
Installation
php artisan ui:add blocks/settings-passkeys
Registry contract
Install confidence
php artisan ui:add blocks/settings-passkeys
writes only the generated targets below. The CLI validates each file hash before writing and
prompts before replacing local changes unless --force is used.
- Version
- 1.1.1
- License
- open
- Stability
- stable
- Contract
- v4
- Foundation
- ≥ 1.0.0
| Type | Generated target |
|---|---|
| blade | resources/views/blocks/settings-passkeys.blade.php |
Registry dependencies
Package dependencies
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.
# Brok UI block: Settings Passkeys (`settings-passkeys`)
A passkey settings screen: each passkey with its created and last-used dates, removal behind a confirmation dialog, and a named Add passkey form that runs the WebAuthn registration.
Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.
## Install
```bash
php artisan ui:add blocks/settings-passkeys
```
## Render it
```blade
<x-blocks.settings-passkeys />
```
## Props
- `heading` (string, default `Passkeys`) — Section heading.
- `subheading` (string, default `Sign in with your fingerprint, face or device PIN instead of a password.`) — Supporting copy under the heading.
- `heading-tag` (string, default `h2`) — Heading level: h1, h2 or h3.
- `passkeys` (array|null, default `null`) — Rows of id, name, createdAt and lastUsedAt (DateTimeInterface or preformatted string; null lastUsedAt reads Never used). Null shows sample rows; [] shows the empty state.
- `options-url` (string|null, default `null`) — Endpoint that returns WebAuthn creation options JSON. Null uses the preset route, else /passkeys/options (POST, receives { name }).
- `verify-url` (string|null, default `null`) — POST endpoint that verifies and stores the new passkey; receives the credential JSON and name. Null uses the preset route, else /passkeys.
- `remove-action` (string|null, default `null`) — DELETE target for one passkey; {id} is replaced by the URL-encoded id. Null uses /user/passkeys/{id} with the laravel-passkeys preset, else /passkeys/{id}.
- `preset` (string|null, default `null`) — Passed to <x-ui.passkey>: 'laravel-passkeys' speaks the laravel/passkeys contract (GET options wrapped in options, the credential under credential).
- `options-method` (GET|POST|null, default `null`) — Passed to <x-ui.passkey>. HTTP method of the options request. Null uses the preset, else POST. GET sends no body.
- `options-key` (string|null, default `null`) — Passed to <x-ui.passkey>. Response key that wraps the options, for example options. Null uses the preset, else publicKey or the top-level object.
- `credential-key` (string|null, default `null`) — Passed to <x-ui.passkey>. Verify body field that holds the credential, for example credential. Null uses the preset, else the credential is the body.
- `body` (array, default `[]`) — Extra verify body fields passed to <x-ui.passkey>.
## Use when
- A security settings page lets people list, add and remove their passkeys against your own WebAuthn endpoints.
## Avoid when
- Password, two-factor and sessions settings; use settings-security (place this block beside it).
## Rules
- Render the installed block with `<x-blocks.settings-passkeys />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.
## Links
- Docs: https://brokui.dev/blocks/settings-passkeys
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/settings-passkeys.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Guidance
Use when
- A security settings page lets people list, add and remove their passkeys against your own WebAuthn endpoints.
Avoid when
- Password, two-factor and sessions settings; use settings-security (place this block beside it).
Anti-patterns
- Do not replace semantic props with conflicting utility classes.
- Do not remove labels, focus styles, or state attributes.
- Anatomy
- Theming hooks
Usage
Render the block as a component, passing data where useful:
<x-blocks.settings-passkeys />
Built from primitives
This block composes installed <brok:*> primitives and semantic design
tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by
editing CSS variables; flip the preview to RTL to confirm it mirrors.
Source
The exact, editable file ui:add writes
into your app. The preview above renders this same source — there are no preview-only blocks.
@props([
'heading' => 'Passkeys',
'subheading' => 'Sign in with your fingerprint, face or device PIN instead of a password.',
'headingTag' => 'h2',
// [['id' => …, 'name' => 'MacBook', 'createdAt' => DateTimeInterface|string,
// 'lastUsedAt' => DateTimeInterface|string|null], …]. Null shows demo rows.
'passkeys' => null,
// WebAuthn registration endpoints (options, POST verify). Null: the
// preset routes, else /passkeys/options and /passkeys.
'optionsUrl' => null,
'verifyUrl' => null,
// DELETE target for one passkey; `{id}` is replaced by the passkey id.
// Null: /user/passkeys/{id} with the laravel-passkeys preset, else
// /passkeys/{id}.
'removeAction' => null,
// Backend contract, passed to the passkey item: 'laravel-passkeys' or null,
// and the single settings a preset makes (see the passkey item).
'preset' => null,
'optionsMethod' => null,
'optionsKey' => null,
'credentialKey' => null,
// Extra verify body fields.
'body' => [],
])
{{--
Settings Passkeys — the passkey list for a security settings page. Each row
shows the name, when it was created and last used, and a Remove action that
asks for confirmation first. "Add passkey" names the new passkey and runs
the WebAuthn registration through <x-ui.passkey>; the page reloads on
success so the list shows it.
--}}
@php
$headingTag = in_array($headingTag, ['h1', 'h2', 'h3'], true) ? $headingTag : 'h2';
$laravelPasskeys = $preset === 'laravel-passkeys';
$optionsUrl ??= $laravelPasskeys ? null : '/passkeys/options';
$verifyUrl ??= $laravelPasskeys ? null : '/passkeys';
$removeAction ??= $laravelPasskeys ? '/user/passkeys/{id}' : '/passkeys/{id}';
$passkeys ??= [
['id' => 'pk_1', 'name' => 'MacBook Pro', 'createdAt' => now()->subMonths(5), 'lastUsedAt' => now()->subHours(3)],
['id' => 'pk_2', 'name' => 'iPhone', 'createdAt' => now()->subMonths(2), 'lastUsedAt' => now()->subDays(9)],
['id' => 'pk_3', 'name' => 'YubiKey 5C', 'createdAt' => now()->subWeeks(1), 'lastUsedAt' => null],
];
$date = static function ($value): array {
if ($value instanceof \DateTimeInterface) {
$carbon = \Illuminate\Support\Carbon::instance($value);
return [$carbon->toIso8601String(), $carbon->translatedFormat('j M Y')];
}
return [null, (string) $value];
};
$uid = 'settings-passkeys-'.\Illuminate\Support\Str::random(6);
@endphp
<x-ui.section data-slot="block-settings-passkeys" aria-labelledby="{{ $uid }}-heading" space="md" {{ $attributes }}>
<div class="mx-auto flex max-w-3xl flex-col gap-6 px-4">
<div>
<{{ $headingTag }} id="{{ $uid }}-heading" class="text-2xl font-bold tracking-tight">{{ __($heading) }}</{{ $headingTag }}>
<p class="mt-2 text-sm text-muted-foreground">{{ __($subheading) }}</p>
</div>
<x-ui.card>
<x-ui.card.header>
<x-ui.card.title>{{ __('Your passkeys') }}</x-ui.card.title>
<x-ui.card.description>{{ __('Remove a passkey you no longer use or do not recognise.') }}</x-ui.card.description>
</x-ui.card.header>
<x-ui.card.content>
@if ($passkeys === [])
<p data-slot="settings-passkeys-empty" class="text-sm text-muted-foreground">{{ __('No passkeys yet. Add one below to sign in without a password.') }}</p>
@else
<ul role="list" class="divide-y divide-border">
@foreach ($passkeys as $passkey)
@php
$id = (string) ($passkey['id'] ?? $loop->index);
$name = (string) ($passkey['name'] ?? __('Passkey'));
[$createdIso, $createdText] = $date($passkey['createdAt'] ?? '');
$lastUsed = $passkey['lastUsedAt'] ?? null;
[$usedIso, $usedText] = $lastUsed === null ? [null, null] : $date($lastUsed);
$formId = $uid.'-remove-'.$loop->index;
@endphp
<li data-slot="settings-passkeys-item" class="flex flex-col gap-4 py-4 first:pt-0 last:pb-0 sm:flex-row sm:items-center">
<div class="flex min-w-0 flex-1 items-center gap-4">
<div class="grid size-10 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground" aria-hidden="true">
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="15" r="4" /><path d="m10.85 12.15 7.65-7.65M18 5l2 2M15 8l2 2" /></svg>
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-foreground">{{ $name }}</p>
<p class="flex flex-wrap gap-x-4 text-xs text-muted-foreground">
<span>{{ __('Created') }} @if ($createdIso)<time datetime="{{ $createdIso }}">{{ $createdText }}</time>@else{{ $createdText }}@endif</span>
<span>
@if ($lastUsed === null)
{{ __('Never used') }}
@else
{{ __('Last used') }} @if ($usedIso)<time datetime="{{ $usedIso }}">{{ $usedText }}</time>@else{{ $usedText }}@endif
@endif
</span>
</p>
</div>
</div>
<x-ui.form :id="$formId" :action="str_replace('{id}', rawurlencode($id), $removeAction)" method="DELETE" class="hidden" />
<x-ui.alert-dialog>
<x-ui.alert-dialog.trigger
aria-label="{{ __('Remove :name', ['name' => $name]) }}"
class="h-9 shrink-0 gap-2 self-start rounded-md px-4 text-sm font-medium text-destructive-text transition-colors hover:bg-destructive-soft focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring focus-visible:ring-offset-[length:var(--ring-offset-width)] focus-visible:ring-offset-background motion-reduce:transition-none sm:self-auto"
>
{{ __('Remove') }}
</x-ui.alert-dialog.trigger>
<x-ui.alert-dialog.content>
<x-ui.alert-dialog.header>
<x-ui.alert-dialog.title>{{ __('Remove the passkey “:name”?', ['name' => $name]) }}</x-ui.alert-dialog.title>
<x-ui.alert-dialog.description>{{ __('You can no longer sign in with it. Keep another passkey or your password so you do not lose access.') }}</x-ui.alert-dialog.description>
</x-ui.alert-dialog.header>
<x-ui.alert-dialog.footer>
<x-ui.alert-dialog.cancel>{{ __('Cancel') }}</x-ui.alert-dialog.cancel>
<x-ui.button type="submit" variant="destructive" :form="$formId">{{ __('Remove passkey') }}</x-ui.button>
</x-ui.alert-dialog.footer>
</x-ui.alert-dialog.content>
</x-ui.alert-dialog>
</li>
@endforeach
</ul>
@endif
</x-ui.card.content>
</x-ui.card>
<x-ui.card>
<x-ui.card.header>
<x-ui.card.title>{{ __('Add a passkey') }}</x-ui.card.title>
<x-ui.card.description>{{ __('Give it a name you will recognise, such as the device it lives on.') }}</x-ui.card.description>
</x-ui.card.header>
<x-ui.card.content>
<x-ui.passkey
mode="register" :options-url="$optionsUrl" :verify-url="$verifyUrl" :reload="true"
:preset="$preset" :options-method="$optionsMethod" :options-key="$optionsKey"
:credential-key="$credentialKey" :body="$body"
>
<form x-on:submit.prevent="start()" class="flex flex-col gap-4 sm:flex-row sm:items-end">
<x-ui.field class="min-w-0 flex-1">
<x-ui.label for="{{ $uid }}-name" required>{{ __('Passkey name') }}</x-ui.label>
<x-ui.input id="{{ $uid }}-name" x-model="name" required maxlength="64" autocomplete="off" :placeholder="__('Work laptop')" />
</x-ui.field>
<x-ui.passkey.button type="submit" class="sm:w-auto">{{ __('Add passkey') }}</x-ui.passkey.button>
</form>
<x-ui.passkey.status />
</x-ui.passkey>
</x-ui.card.content>
</x-ui.card>
</div>
</x-ui.section>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.1.1 - Status
- Stable
- License
-
open - Deprecation
- Not deprecated
- Contract
-
v4 - Foundation
-
≥ 1.0.0