Frontend

Blade views

The package ships working Blade views for both flows and both emails. They exist so the package works the moment you install it — they are not meant to be the final look of your app.


Two ways to change them

Point the config at your own view

Each key takes a view name. null means "use the package default".

// config/passwordless.php
'views' => [
    'magic_link_request' => null,  // GET /auth/magic-link
    'magic_link_sent'    => null,  // after POST /auth/magic-link
    'magic_link_email'   => null,  // email notification
    'login_code_request' => null,  // GET /auth/code
    'login_code_verify'  => null,  // GET /auth/code/verify
    'login_code_email'   => null,  // email notification
],

For example, to keep everything but replace the magic link email:

'views' => [
    'magic_link_email' => 'emails.auth.magic-link',
],

This is the better option when you want your own views in their natural place in resources/views, under your own layout.

Publish and edit in place

php artisan vendor:publish --tag="laravel-passwordless-views"

Everything lands in resources/views/vendor/laravel-passwordless/ and takes precedence over the package copies automatically — no config change needed.

resources/views/vendor/laravel-passwordless/
├── layouts/auth.blade.php
├── magic-link/request.blade.php
├── magic-link/sent.blade.php
├── login-code/request.blade.php
├── login-code/verify.blade.php
├── emails/magic-link.blade.php
└── emails/login-code.blade.php

This is the better option when you want the working forms as a starting point rather than writing them from scratch.

You can mix the two

Publishing does not disable the config keys. Publish the set, then still point one key elsewhere if a particular screen wants to live somewhere else. Config wins where it is set; the published file wins everywhere else; the package default is the last resort.


What each view receives

KeyVariableTypeDescription
magic_link_email$urlstringThe fully-signed magic link URL
magic_link_email$expiresMinsintTTL in minutes
login_code_email$codestringThe plain-text code
login_code_email$expiresMinsintTTL in minutes
login_code_verify$emailstringThe pending address, so the screen can say where the code went

The remaining form views receive nothing — they post to named routes and read errors from the standard error bag.


Writing your own forms

Nothing about the forms is special. They post to the package's named routes and surface validation errors on the email and code fields.

<form method="POST" action="{{ route('passwordless.magic-link.send') }}">
    @csrf
    <input type="email" name="email" value="{{ old('email') }}" required>
    @error('email') <p>{{ $message }}</p> @enderror
    <button type="submit">Email me a link</button>
</form>

Login code request

<form method="POST" action="{{ route('passwordless.login-code.send') }}">
    @csrf
    <input type="email" name="email" value="{{ old('email') }}" required>
    @error('email') <p>{{ $message }}</p> @enderror
    <button type="submit">Email me a code</button>
</form>

Login code verify

<form method="POST" action="{{ route('passwordless.login-code.authenticate') }}">
    @csrf
    <input type="text" name="code" inputmode="numeric" autocomplete="one-time-code" required>
    @error('code') <p>{{ $message }}</p> @enderror
    <button type="submit">Verify</button>
</form>

The pending email address is held in the session under passwordless.pending_email and passed to the view as $email, so the verify form does not need to resubmit it.

autocomplete="one-time-code"

Worth setting on the code input. It is what lets iOS and Android offer the code from the notification as a keyboard suggestion, which removes the main annoyance of the code flow. Pair it with inputmode="numeric" while your charset is numeric.


Using Inertia instead

If your app renders through Inertia, the components config replaces views entirely. See Inertia.

Previous
Routes