<x-widget.auth-layout>
Auth layout Blade component for Laravel and Livewire
A sign-in, sign-up or password page: your brand, a heading, the form and a line under it. Split puts the form beside a panel in your brand's colour on large screens, with a quote or a few words at its foot, and gives the form the page on phones; centered sets it on a card in the middle. The form rises into place as the page paints, unless motion is reduced. No script, so it works inside Livewire components as it is.
php artisan bladewell:add auth-layout
- Also adds
- Nothing else. It stands alone.
Usage
Livewire
The layout has no script, so inside a Livewire component it renders like any other markup: bind the fields with wire:model and submit with wire:submit. Errors from $this->validate() show under each field.
<x-widget.auth-layout title="Welcome back" subtitle="Sign in to your account.">
<form wire:submit="login" class="flex flex-col gap-5">
<x-widget.text-input wire:model="email" type="email" label="Email" autocomplete="username" required />
<x-widget.password wire:model="password" label="Password" required />
<x-widget.checkbox wire:model="remember" label="Remember me" />
<x-widget.button type="submit" class="w-full">Sign in</x-widget.button>
</form>
</x-widget.auth-layout>
Examples
These examples also use Text input, Password, Checkbox and Button, which Auth layout doesn't need on its own. To use them as they are:
php artisan bladewell:add auth-layout text-input password checkbox button
Sign in
A sign-in page, with your brand's panel beside the form on a large screen and the form alone on a phone. It posts to the login route Laravel's starter kits name, so it works with Fortify or your own controller as it is. In your app, leave out the box around it: it fills the window.
Show code Hide code
<div class="border-line h-[44rem] overflow-hidden rounded-2xl border">
<x-widget.auth-layout title="Welcome back" subtitle="Sign in to your Acme account.">
<x-slot:brand>
<a href="{{ url('/') }}" class="focus-visible:ring-primary flex items-center gap-3 rounded-lg outline-none focus-visible:ring-2">
<span aria-hidden="true" class="bg-primary-fill text-on-primary grid size-8 place-items-center rounded-lg text-sm font-bold">A</span>
<span class="font-semibold">Acme</span>
</a>
</x-slot:brand>
<form method="POST" action="{{ route('login') }}" class="flex flex-col gap-5">
@csrf
<x-widget.text-input name="email" type="email" label="Email" autocomplete="username" required />
<x-widget.password name="password" label="Password" required />
<div class="flex items-center justify-between gap-4">
<x-widget.checkbox name="remember" label="Remember me" />
<a href="{{ route('password.request') }}" class="text-link hover:text-link-hover shrink-0 text-sm font-medium whitespace-nowrap underline-offset-4 hover:underline">Forgot password?</a>
</div>
<x-widget.button type="submit" class="mt-2 w-full">Sign in</x-widget.button>
</form>
<x-slot:footer>
No account yet? <a href="{{ route('register') }}" class="text-link hover:text-link-hover font-medium underline-offset-4 hover:underline">Create one</a>
</x-slot:footer>
<x-slot:aside>
<figure class="max-w-md">
<blockquote class="text-xl leading-relaxed font-medium xl:text-2xl">“We moved our whole back office to Acme in a week. Our team hasn't looked back.”</blockquote>
<figcaption class="mt-6 flex items-center gap-3 text-sm">
<span aria-hidden="true" class="bg-on-primary text-primary-fill grid size-10 place-items-center rounded-full font-semibold">MC</span>
<span>
<span class="block font-semibold">Maya Chen</span>
<span class="block">Head of Operations, Northwind</span>
</span>
</figcaption>
</figure>
</x-slot:aside>
</x-widget.auth-layout>
</div>
Sign up
A sign-up page. The password field shows its rules as a checklist while it's typed; match them to your Form Request, e.g. Password::min(8)->numbers(). The terms box is required, so the browser asks for it before the form is sent.
Show code Hide code
<div class="border-line h-[54rem] overflow-hidden rounded-2xl border">
<x-widget.auth-layout title="Create your account" subtitle="Start your free 14-day trial. No card needed.">
<x-slot:brand>
<a href="{{ url('/') }}" class="focus-visible:ring-primary flex items-center gap-3 rounded-lg outline-none focus-visible:ring-2">
<span aria-hidden="true" class="bg-primary-fill text-on-primary grid size-8 place-items-center rounded-lg text-sm font-bold">A</span>
<span class="font-semibold">Acme</span>
</a>
</x-slot:brand>
<form method="POST" action="{{ route('register') }}" class="flex flex-col gap-5">
@csrf
<x-widget.text-input name="name" label="Name" autocomplete="name" required />
<x-widget.text-input name="email" type="email" label="Email" autocomplete="email" required />
<x-widget.password name="password" label="Password" new :min="8" numbers required />
<x-widget.password name="password_confirmation" label="Confirm password" autocomplete="new-password" :toggle="false" required />
<x-widget.checkbox name="terms" required>
I agree to the <a href="#terms" class="text-link hover:text-link-hover underline underline-offset-4">terms</a> and <a href="#privacy" class="text-link hover:text-link-hover underline underline-offset-4">privacy policy</a>
</x-widget.checkbox>
<x-widget.button type="submit" class="mt-2 w-full">Create account</x-widget.button>
</form>
<x-slot:footer>
Already have an account? <a href="{{ route('login') }}" class="text-link hover:text-link-hover font-medium underline-offset-4 hover:underline">Sign in</a>
</x-slot:footer>
<x-slot:aside>
<p class="text-sm font-semibold tracking-wide uppercase">Everything in one place</p>
<p class="mt-3 max-w-md text-2xl leading-snug font-semibold xl:text-3xl">Orders, customers and reports, ready the moment you sign up.</p>
<ul class="mt-8 flex flex-col gap-3 text-sm">
<li>Unlimited team members</li>
<li>Exports to CSV and Excel</li>
<li>Cancel any time</li>
</ul>
</x-slot:aside>
</x-widget.auth-layout>
</div>
Forgot password
variant="centered": the form on a card in the middle of a tinted page, for a short form or an app without a brand panel. It posts to password.email, the route Laravel's starter kits send the reset link from.
Forgot your password?
Enter your email and we'll send you a link to choose a new one.
Show code Hide code
<div class="border-line h-[34rem] overflow-hidden rounded-2xl border">
<x-widget.auth-layout variant="centered" title="Forgot your password?" subtitle="Enter your email and we'll send you a link to choose a new one.">
<x-slot:brand>
<a href="{{ url('/') }}" class="focus-visible:ring-primary flex items-center gap-3 rounded-lg outline-none focus-visible:ring-2">
<span aria-hidden="true" class="bg-primary-fill text-on-primary grid size-8 place-items-center rounded-lg text-sm font-bold">A</span>
<span class="font-semibold">Acme</span>
</a>
</x-slot:brand>
<form method="POST" action="{{ route('password.email') }}" class="flex flex-col gap-5">
@csrf
<x-widget.text-input name="email" type="email" label="Email" autocomplete="email" required />
<x-widget.button type="submit" class="w-full">Email me a reset link</x-widget.button>
</form>
<x-slot:footer>
Remembered it? <a href="{{ route('login') }}" class="text-link hover:text-link-hover font-medium underline-offset-4 hover:underline">Back to sign in</a>
</x-slot:footer>
</x-widget.auth-layout>
</div>
Props
Other attributes, such as autocomplete or data-*, are passed through to the element. class styles the component's outer wrapper.
<x-widget.auth-layout>
| Prop | Default | Description |
|---|---|---|
| title |
null
|
The page's heading, its h1 ("Welcome back"). |
| subtitle |
null
|
A line under the heading ("Sign in to your account."). |
| variant |
'split'
|
split: the form beside a panel in your brand's colour from lg up, the form alone below that. centered: the form on a card in the middle of a tinted page. |
Accessibility
All 3 examples above are checked with axe-core against the WCAG 2.2 A and AA rules, in the light theme and the dark one, both as the page draws and with each popover, dialog, toast and tooltip opened, on every change. A change that fails can't be merged. Where axe can't decide, such as contrast on SVG text, the test measures the colours itself instead of letting it pass.
Automated checks can't judge everything: how it sounds in a screen reader, and how it feels to use from the keyboard, still need a person. Check those on your own pages too.
Source
What bladewell:add auth-layout writes to your app with the default namespaces. Prefer to copy by hand? Take these files, plus the ones from
the components it also adds , and the theme and base CSS.
resources/views/components/widget/auth-layout/index.blade.php Show
@props([
// The page's heading, its h1 ("Welcome back").
'title' => null,
// A line under the heading ("Sign in to your account.").
'subtitle' => null,
// split: the form beside a panel in your brand's colour from lg up, the form alone below that. centered: the form
// on a card in the middle of a tinted page.
'variant' => 'split',
])
@php
// A typo fails loudly, naming the values that work, instead of quietly rendering something else.
if (! in_array($variant, ['split', 'centered'], true)) {
throw new \InvalidArgumentException("Unknown variant [{$variant}] for <x-widget.auth-layout>. Use one of: split, centered.");
}
$split = $variant === 'split';
// The form and its heading rise into place as the page first paints (@starting-style), unless motion is reduced.
$enter = 'transition-[opacity,translate] duration-700 ease-[cubic-bezier(0.22,1,0.36,1)] starting:translate-y-3 starting:opacity-0 motion-reduce:transition-none';
@endphp
{{--
A sign-in, sign-up or password page: your brand, a heading, the form (the slot) and a line under it, such as a link
to the other page.
split puts a panel in your brand's colour beside the form on large screens, with whatever the aside slot holds (a
quote, a few words about the product) at its foot; on smaller screens the form has the page to itself. centered sets
the form on a card in the middle of a tinted page.
It fills the window (h-dvh), or its parent where that has a height of its own (max-h-full), like the examples here,
and the form scrolls inside it when it's longer than the window. No script and no inline style.
--}}
<div
data-auth-layout
{{ $attributes->class([
'text-foreground relative flex h-dvh max-h-full',
'bg-surface' => $split,
'flex-col overflow-y-auto bg-[color-mix(in_oklab,var(--color-field)_45%,var(--color-surface))]' => ! $split,
]) }}
>
@if ($split)
<main class="flex min-w-0 flex-1 flex-col overflow-y-auto px-6 py-6 sm:px-12">
@isset($brand)
<div class="flex shrink-0 items-center gap-3">{{ $brand }}</div>
@endisset
<div class="flex flex-1 items-center justify-center py-6">
<div class="{{ $enter }} w-full max-w-sm">
@if ($title)
<h1 class="text-2xl font-semibold tracking-tight sm:text-3xl">{{ $title }}</h1>
@endif
@if ($subtitle)
<p class="text-foreground/70 mt-2 text-sm">{{ $subtitle }}</p>
@endif
<div @class(['mt-8' => $title || $subtitle])>{{ $slot }}</div>
@isset($footer)
<div class="text-foreground/70 mt-8 text-center text-sm">{{ $footer }}</div>
@endisset
</div>
</div>
</main>
{{-- The brand panel, from lg up. Its glow sits in the top half, clear of the text at its foot, so that text is
always on the plain fill: readable, and measurable by contrast checks. --}}
<aside class="bg-primary-fill text-on-primary relative m-3 hidden w-1/2 shrink-0 flex-col justify-end overflow-hidden rounded-3xl transition-opacity duration-1000 starting:opacity-0 motion-reduce:transition-none lg:flex">
<div aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-0 h-1/2">
<div class="bg-on-primary/25 absolute -end-24 -top-28 size-96 rounded-full blur-3xl"></div>
<div class="absolute start-1/4 -top-16 size-72 rounded-full bg-black/25 blur-3xl"></div>
<div class="absolute inset-0 bg-[radial-gradient(color-mix(in_oklab,var(--color-on-primary)_30%,transparent)_1px,transparent_1px)] bg-size-[22px_22px] opacity-60"></div>
</div>
@isset($aside)
<div class="relative p-10 xl:p-14">{{ $aside }}</div>
@endisset
</aside>
@else
<main class="flex min-h-full flex-col items-center justify-center gap-8 px-4 py-10">
@isset($brand)
<div class="flex items-center justify-center gap-3">{{ $brand }}</div>
@endisset
<div class="{{ $enter }} border-line bg-surface w-full max-w-md rounded-2xl border p-6 shadow-sm sm:p-8">
@if ($title)
<h1 class="text-2xl font-semibold tracking-tight">{{ $title }}</h1>
@endif
@if ($subtitle)
<p class="text-foreground/70 mt-2 text-sm">{{ $subtitle }}</p>
@endif
<div @class(['mt-6' => $title || $subtitle])>{{ $slot }}</div>
</div>
@isset($footer)
<div class="text-foreground/70 text-center text-sm">{{ $footer }}</div>
@endisset
</main>
@endif
</div>