Skip to content
Brok UI

Loading…

No results

Settings

Open source

Account settings screens — a grouped account form, a notification-preferences matrix and a connected-apps integrations list.

Preview

6 of 6

resources/views/blocks/settings-passkeys.blade.php Blade
@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

terminal
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

section card button alert-dialog form field label input passkey

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.

settings-passkeys.md
# 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
root heading passkey list remove confirmation add form
Theming hooks
card alert-dialog button destructive passkey status tones

Usage

Render the block as a component, passing data where useful:

resources/views/welcome.blade.php Blade
<x-blocks.settings-passkeys />

Built from primitives

Composes <brok:*> Semantic tokens Re-themeable RTL-ready

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.

resources/views/blocks/settings-passkeys.blade.php Blade
@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