Updated for Laravel 12, Livewire 3, and PHP 8.2–8.4
If you build Laravel apps for real clients, authentication is one of the first features you ship. In Laravel 12, we still have the lightweight Laravel Breeze starter, and with Livewire 3 we can turn the login experience into a smooth, reactive form without touching a single line of JavaScript.
In this guide you’ll set up a fresh Laravel 12 project, install Breeze, add Livewire 3, and rebuild the login form as a Livewire component with real-time validation and a clean UX.
1. Prerequisites
You’ll need the following to follow along:
- PHP 8.2 or higher (Laravel 12 supports PHP 8.2–8.4)
- Composer installed globally
- Node.js and npm (for building frontend assets)
- A database (MySQL, PostgreSQL, SQLite, etc.)
php -v
If PHP is below 8.2, upgrade before continuing.
2. Create a New Laravel 12 Project
laravel new breeze-livewire-auth
When the installer asks for a starter kit, choose None. We’ll install Breeze manually, then enhance it with Livewire.
cd breeze-livewire-auth
php artisan migrate
3. Install Laravel Breeze on Laravel 12
Breeze is Laravel’s minimal authentication starter kit. It scaffolds login, registration, password reset, email verification and a simple Tailwind-based UI.
composer require laravel/breeze --dev
php artisan breeze:install blade
Now install and build the frontend assets:
npm install
npm run build # or: npm run dev
Start the server with php artisan serve and visit /login to confirm that Breeze authentication works.
4. Install Livewire 3
Next, pull in Livewire 3 so we can make the login experience reactive.
composer require livewire/livewire
Add Livewire styles and scripts to Breeze’s app layout:
<!-- resources/views/layouts/app.blade.php -->
<head>
...
@livewireStyles
</head>
<body>
...
@livewireScripts
</body>
5. Build a Livewire Login Component
Instead of posting the Breeze login form to a controller, we’ll move the logic into a Livewire component for a smoother experience.
php artisan make:livewire Auth.LoginForm
5.1 Component class
<?php
namespace App\Livewire\Auth;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class LoginForm extends Component
{
public string $email = '';
public string $password = '';
public bool $remember = false;
protected array $rules = [
'email' => ['required', 'email'],
'password' => ['required'],
];
public function updated($property)
{
$this->validateOnly($property);
}
public function login()
{
$credentials = $this->validate();
if (! Auth::attempt([
'email' => $this->email,
'password' => $this->password,
], $this->remember)) {
$this->addError('email', 'The provided credentials do not match our records.');
return;
}
session()->regenerate();
return redirect()->intended(route('dashboard'));
}
public function render()
{
return view('livewire.auth.login-form');
}
}
5.2 Blade view
<!-- resources/views/livewire/auth/login-form.blade.php -->
<div class="w-full max-w-sm mx-auto space-y-4">
<header class="space-y-1">
<h1 class="text-xl font-semibold text-gray-900">Log in</h1>
<p class="text-xs text-gray-500">
Use your account to access the dashboard.
</p>
</header>
<form wire:submit.prevent="login" class="space-y-4">
<!-- Email -->
<div class="space-y-1">
<label for="email" class="block text-xs font-medium text-gray-700">
Email
</label>
<input
id="email"
type="email"
wire:model.defer="email"
autocomplete="email"
class="block w-full rounded-md border-gray-300 text-sm shadow-sm
focus:border-indigo-500 focus:ring-indigo-500"
>
@error('email')
<p class="text-xs text-red-600 mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Password -->
<div class="space-y-1">
<label for="password" class="block text-xs font-medium text-gray-700">
Password
</label>
<input
id="password"
type="password"
wire:model.defer="password"
autocomplete="current-password"
class="block w-full rounded-md border-gray-300 text-sm shadow-sm
focus:border-indigo-500 focus:ring-indigo-500"
>
@error('password')
<p class="text-xs text-red-600 mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Remember + Forgot password -->
<div class="flex flex-col xs:flex-row xs:items-center xs:justify-between gap-2">
<label class="inline-flex items-center space-x-2 text-xs text-gray-700">
<input
type="checkbox"
wire:model="remember"
class="rounded border-gray-300 text-indigo-600 shadow-sm
focus:ring-indigo-500"
>
<span>Remember me</span>
</label>
@if (Route::has('password.request'))
<a href="{{ route('password.request') }}"
class="text-xs text-indigo-600 hover:text-indigo-500">
Forgot password?
</a>
@endif
</div>
<button
type="submit"
class="inline-flex w-full items-center justify-center rounded-md
bg-indigo-600 px-3 py-2 text-sm font-medium text-white
hover:bg-indigo-700 focus:outline-none focus:ring-2
focus:ring-indigo-500 focus:ring-offset-2"
>
Log in
</button>
</form>
</div>
Now update Breeze’s login view to use the component:
<!-- resources/views/auth/login.blade.php -->
<x-guest-layout>
<div class="mt-6">
<livewire:auth.login-form />
</div>
</x-guest-layout>
6. Security and Best Practices
- Always serve production apps over HTTPS.
- Keep Laravel, Breeze, and Livewire updated.
- Use Laravel’s built-in rate limiting for login attempts.
- Enable email verification for user-facing apps.
- Consider 2FA or social login for larger or high-risk projects.
7. Conclusion
Breeze gives you a clean, minimal starting point for authentication in Laravel 12, while Livewire 4 brings it to life with a reactive, modern UX. Together they form a powerful stack that lets you move quickly, especially when you work solo or in a small team.
From here you can extend the setup with registration, email verification, password reset flows, or even two-factor authentication — all while keeping your stack simple and Laravel-first.
Need a custom Laravel authentication system or dashboard for your business? Feel free to reach out — this is exactly what I help clients build.