Authentication
This recipe demonstrates building a login and registration system with reactive validation feedback, loading states, and secure session-based authentication using Laravel's built-in Auth features.
What We're Building
A complete authentication flow with:
- Login form with email and password
- Registration form with validation
- Real-time validation feedback
- Password visibility toggle
- Loading states during submission
- "Remember me" functionality
Login Form
Blade Template
Create resources/views/auth/login.blade.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign In</title>
@hyper
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50 min-h-screen flex items-center justify-center">
<div class="w-full max-w-md">
<div class="bg-white rounded-lg shadow-lg p-8">
<h2 class="text-2xl font-bold mb-6 text-gray-900">Sign In</h2>
<form @signals([
'email' => old('email', ''),
'password' => '',
'remember' => false,
'_showPassword' => false,
'_submitting' => false,
'errors' => []
])
data-on:submit__prevent="$_submitting = true; @postx('/login')">
<!-- Email Field -->
<div class="mb-4">
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">
Email Address
</label>
<input
type="email"
id="email"
data-bind="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="you@example.com"
data-attr:disabled="$_submitting"
required
/>
<div data-error="email" class="mt-1 text-sm text-red-600"></div>
</div>
<!-- Password Field -->
<div class="mb-4">
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<div class="relative">
<input
id="password"
data-bind="password"
data-attr:type="$_showPassword ? 'text' : 'password'"
class="w-full px-3 py-2 pr-10 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter your password"
data-attr:disabled="$_submitting"
required
/>
<button
type="button"
data-on:click="$_showPassword = !$_showPassword"
class="absolute right-3 top-2.5 text-gray-500 hover:text-gray-700">
<span data-text="$_showPassword ? '🙈' : '👁️'"></span>
</button>
</div>
<div data-error="password" class="mt-1 text-sm text-red-600"></div>
</div>
<!-- Remember Me & Forgot Password -->
<div class="flex items-center justify-between mb-6">
<label class="flex items-center">
<input
type="checkbox"
data-bind="remember"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
data-attr:disabled="$_submitting"
/>
<span class="ml-2 text-sm text-gray-700">Remember me</span>
</label>
<a href="/password/reset" class="text-sm text-blue-600 hover:text-blue-800">
Forgot password?
</a>
</div>
<!-- Submit Button -->
<button
type="submit"
data-attr:disabled="$_submitting"
data-class="{
'opacity-50': $_submitting,
'cursor-not-allowed': $_submitting
}"
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
<span data-show="!$_submitting">Sign In</span>
<span data-show="$_submitting">Signing in...</span>
</button>
<!-- Registration Link -->
<p class="mt-4 text-center text-sm text-gray-600">
Don't have an account?
<a href="/register" class="text-blue-600 hover:text-blue-800 font-medium">
Sign up
</a>
</p>
</form>
</div>
</div>
</body>
</html>Controller
Create app/Http/Controllers/Auth/LoginController.php:
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function showLoginForm()
{
return view('auth.login');
}
public function login()
{
$validated = signals()->validate([
'email' => 'required|email',
'password' => 'required|string',
]);
$remember = signals('remember', false);
if (Auth::attempt($validated, $remember)) {
request()->session()->regenerate();
return redirect('/dashboard');
}
return hyper()->signals([
'errors' => [
'email' => ['These credentials do not match our records.']
],
'password' => '',
'_submitting' => false
]);
}
public function logout()
{
Auth::logout();
request()->session()->invalidate();
request()->session()->regenerateToken();
return redirect('/login');
}
}Routes
Add to routes/web.php:
use App\Http\Controllers\Auth\LoginController;
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login');
Route::post('/login', [LoginController::class, 'login']);
Route::post('/logout', [LoginController::class, 'logout'])->name('logout');How It Works
Signal Initialization
The @signals directive (Hyper) initializes the form state:
@signals([
'email' => old('email', ''),
'password' => '',
'remember' => false,
'_showPassword' => false, // Local signal (Datastar)
'_submitting' => false, // Local signal (Datastar)
'errors' => []
])Local signals (prefixed with _) are a Datastar feature. They stay in the browser and aren't sent to the server, making them perfect for UI-only state like password visibility and loading indicators.
Two-Way Binding
data-bind (Datastar) creates reactive two-way binding between inputs and signals:
<input data-bind="email" />When the user types, the email signal updates automatically. When the server updates the signal, the input value changes automatically.
Password Visibility Toggle
Datastar's data-attr:type dynamically changes the input type:
<input data-attr:type="$_showPassword ? 'text' : 'password'" />
<button data-on:click="$_showPassword = !$_showPassword">
<span data-text="$_showPassword ? '🙈' : '👁️'"></span>
</button>The button toggles the _showPassword signal, which changes the input between password and text types.
Form Submission
The form uses Datastar's data-on:submit with the __prevent modifier to stop default submission:
data-on:submit__prevent="$_submitting = true; @postx('/login')"Breakdown:
data-on:submit- Datastar listens for the submit event__prevent- Datastar modifier that prevents default browser submission$_submitting = true- Sets loading state@postx('/login')- Hyper's CSRF-protected POST action
Validation Errors
Hyper's data-error attribute automatically displays validation errors from Laravel:
<input data-bind="email" />
<div data-error="email"></div>When signals()->validate() fails in the controller, Hyper creates an errors signal with Laravel's error structure. The data-error attribute shows the first error for that field.
Loading States
Multiple Datastar attributes work together to show loading states:
<button
data-attr:disabled="$_submitting"
data-class="{
'opacity-50': $_submitting,
'cursor-not-allowed': $_submitting
}"
<span data-show="!$_submitting">Sign In</span>
<span data-show="$_submitting">Signing in...</span>
</button>data-attr:disabled(Datastar) - Conditionally adds thedisabledattributedata-class:*(Datastar) - Conditionally applies CSS classesdata-show(Datastar) - Toggles visibility withdisplay: none
Successful Authentication
After successful login, Laravel's Auth facade handles session creation:
if (Auth::attempt($validated, $remember)) {
request()->session()->regenerate();
return redirect('/dashboard');
}redirect()->hyperNavigate() (Hyper) performs a full page redirect using JavaScript's window.location, properly handling Laravel's session regeneration for security.
Registration Form
Blade Template
Create resources/views/auth/register.blade.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Account</title>
@hyper
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50 min-h-screen flex items-center justify-center py-12">
<div class="w-full max-w-md">
<div class="bg-white rounded-lg shadow-lg p-8">
<h2 class="text-2xl font-bold mb-6 text-gray-900">Create Account</h2>
<form @signals([
'name' => old('name', ''),
'email' => old('email', ''),
'password' => '',
'password_confirmation' => '',
'_showPassword' => false,
'_submitting' => false,
'errors' => []
])
data-on:submit__prevent="$_submitting = true; @postx('/register')">
<!-- Name Field -->
<div class="mb-4">
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">
Full Name
</label>
<input
type="text"
id="name"
data-bind="name"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="John Doe"
data-attr:disabled="$_submitting"
required
/>
<div data-error="name" class="mt-1 text-sm text-red-600"></div>
</div>
<!-- Email Field -->
<div class="mb-4">
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">
Email Address
</label>
<input
type="email"
id="email"
data-bind="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="you@example.com"
data-attr:disabled="$_submitting"
required
/>
<div data-error="email" class="mt-1 text-sm text-red-600"></div>
</div>
<!-- Password Field -->
<div class="mb-4">
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<div class="relative">
<input
id="password"
data-bind="password"
data-attr:type="$_showPassword ? 'text' : 'password'"
class="w-full px-3 py-2 pr-10 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="At least 8 characters"
data-attr:disabled="$_submitting"
required
/>
<button
type="button"
data-on:click="$_showPassword = !$_showPassword"
class="absolute right-3 top-2.5 text-gray-500 hover:text-gray-700">
<span data-text="$_showPassword ? '🙈' : '👁️'"></span>
</button>
</div>
<div data-error="password" class="mt-1 text-sm text-red-600"></div>
</div>
<!-- Password Confirmation -->
<div class="mb-6">
<label for="password_confirmation" class="block text-sm font-medium text-gray-700 mb-1">
Confirm Password
</label>
<input
id="password_confirmation"
data-bind="password_confirmation"
data-attr:type="$_showPassword ? 'text' : 'password'"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Re-enter your password"
data-attr:disabled="$_submitting"
required
/>
<div data-error="password_confirmation" class="mt-1 text-sm text-red-600"></div>
</div>
<!-- Submit Button -->
<button
type="submit"
data-attr:disabled="$_submitting"
data-class:opacity-50="$_submitting"
data-class:cursor-not-allowed="$_submitting"
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
<span data-show="!$_submitting">Create Account</span>
<span data-show="$_submitting">Creating account...</span>
</button>
<!-- Login Link -->
<p class="mt-4 text-center text-sm text-gray-600">
Already have an account?
<a href="/login" class="text-blue-600 hover:text-blue-800 font-medium">
Sign in
</a>
</p>
</form>
</div>
</div>
</body>
</html>Controller
Create app/Http/Controllers/Auth/RegisterController.php:
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class RegisterController extends Controller
{
public function showRegistrationForm()
{
return view('auth.register');
}
public function register()
{
$validated = signals()->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'password' => 'required|string|min:8|confirmed',
], [
'email.unique' => 'This email address is already registered.',
'password.confirmed' => 'The password confirmation does not match.',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
Auth::login($user);
request()->session()->regenerate();
return redirect('/dashboard');
}
}Routes
Add to routes/web.php:
use App\Http\Controllers\Auth\RegisterController;
Route::get('/register', [RegisterController::class, 'showRegistrationForm'])->name('register');
Route::post('/register', [RegisterController::class, 'register']);Key Takeaways
1. Clear Attribution:
- Datastar provides:
data-bind,data-on:*,data-attr:*,data-class:*,data-show,data-textand local signals (underscore prefix) - Hyper provides:
@signalsdirective,data-errorattribute,@postxaction, andsignals()->validate()helper
2. Local Signals for UI State: Using _showPassword and _submitting (Datastar's local signals) reduces server payload since they stay in the browser.
3. Standard Laravel Authentication: Hyper works seamlessly with Laravel's Auth facade and session-based authentication. No special adaptations needed.
4. Validation Integration:signals()->validate() (Hyper) uses Laravel's standard validation rules and automatically creates the errors signal for frontend display.
Enhancements
Real-Time Email Availability Check
Add instant feedback when checking if an email is taken:
<input
data-bind="email"
data-on:input__debounce.500ms="@get('/check-email?email=' + encodeURIComponent($email))"
/>In the web.php:
Route::get('/check-email', function () {
$email = signals()->get('email'); // Get the email from ?email=...
// Check if any user already has this email
$exists = User::where('email', $email)->exists();
return hyper()->signals([
'errors' => $exists
? []
: ['This email is already taken.']
]);
});The __debounce.500ms modifier (Datastar) waits 500ms after the user stops typing before making the request. You could check the error at the level of the network tab.
Password Strength Indicator
Use Datastar's data-computed:* for reactive password strength:
<div class="flex flex-col space-y-1">
<input
type="password"
data-bind="password"
placeholder="Enter your password"
class="border p-1 rounded"
/>
<div data-computed:strength="
$password.length === 0 ? 'none' :
$password.length < 8 ? 'weak' :
$password.length < 12 ? 'medium' : 'strong'
">
<div class="mt-2 text-sm">
<span data-show="$strength === 'weak'" class="text-red-600">
Weak password
</span>
<span data-show="$strength === 'medium'" class="text-yellow-600">
Medium strength
</span>
<span data-show="$strength === 'strong'" class="text-green-600">
Strong password
</span>
</div>
</div>
</div>The data-computed:strength (Datastar) creates a computed signal that automatically updates when the password changes.

