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 4 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 4, 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 4
Next, pull in Livewire 4 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.