Skip to content

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:

blade
<!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
<?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:

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:

blade
@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:

blade
<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:

blade
<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:

blade
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:

blade
<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:

blade
<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 the disabled attribute
  • data-class:* (Datastar) - Conditionally applies CSS classes
  • data-show (Datastar) - Toggles visibility with display: none

Successful Authentication ​

After successful login, Laravel's Auth facade handles session creation:

php
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:

blade
<!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
<?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:

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-text and local signals (underscore prefix)
  • Hyper provides: @signals directive, data-error attribute, @postx action, and signals()->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:

blade
<input
    data-bind="email"
    data-on:input__debounce.500ms="@get('/check-email?email=' + encodeURIComponent($email))"
/>

In the web.php:

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:

blade
 <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.