We all are used to default Laravel Auth login/register forms – as separate /login and /register URLs. But sometimes it’s needed to have them as modal popups instead of separate pages. How to implement it in Laravel, including showing the validation errors in modals?

Here’s a short video of what we’ll build in this article:


So, typical default Laravel project with Auth generated, but the Login/Register links on top-right will lead to modal popups instead of separate pages.

Also, there will be two different types of submitting the form:

  • Login form will submit to the same default Laravel POST /login, redirect back in case of error, and auto-show the popup again with error message
  • Register form will submit via AJAX call to /register and show error messages immediately, without page refresh

Step 1. Login Form: Bootstrap Modal Popup and Link

First, let’s create this file: resources/views/partials/login.blade.php

We will almost copy-paste the forms from default resources/views/auth/login.blade.php file to there.

<div class="modal fade" id="loginModal" tabindex="-1" role="dialog" aria-labelledby="loginModal" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="loginModal">{{ __('Login') }}</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">×</span>
                </button>
            </div>
            <div class="modal-body">
                <form method="POST" action="{{ route('login') }}">
                    @csrf

                    <div class="form-group row">
                        <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                        <div class="col-md-6">
                            <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>

                            @error('email')
                                <span class="invalid-feedback" role="alert">
                                    <strong>{{ $message }}</strong>
                                </span>
                            @enderror
                        </div>
                    </div>

                    <div class="form-group row">
                        <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

                        <div class="col-md-6">
                            <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">

                            @error('password')
                                <span class="invalid-feedback" role="alert">
                                    <strong>{{ $message }}</strong>
                                </span>
                            @enderror
                        </div>
                    </div>

                    <div class="form-group row">
                        <div class="col-md-6 offset-md-4">
                            <div class="form-check">
                                <input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>

                                <label class="form-check-label" for="remember">
                                    {{ __('Remember Me') }}
                                </label>
                            </div>
                        </div>
                    </div>

                    <div class="form-group row mb-0">
                        <div class="col-md-8 offset-md-4">
                            <button type="submit" class="btn btn-primary">
                                {{ __('Login') }}
                            </button>

                            @if (Route::has('password.request'))
                                <a class="btn btn-link" href="{{ route('password.request') }}">
                                    {{ __('Forgot Your Password?') }}
                                </a>
                            @endif
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>

You will recognize good old Login form, but with Bootstrap modal classes.

The most important part is div id=”loginModal” identifier, that’s exactly how we would identify the popup.

So now, in resources/views/layouts/app.blade.php we make this change.

From:

<a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>

To:

<a class="nav-link" 
    style="cursor: pointer" 
    data-toggle="modal" 
    data-target="#loginModal">{{ __('Login') }}</a>

And we also need to include this, right? So, somewhere in resources/views/layouts/app.blade.php, add this:

@include('partials.login')

Visually, result is this – when we click Login, modal appears:


Step 2. Login Submit and Validation Errors

As mentioned above, for Login example we will leave form action to be the same typical POST:

<form method="POST" action="{{ route('login') }}">

So, if login is successful, then the same Laravel default logic would apply – redirect to the homepage after login.

The question then becomes – how/where to show validation errors, if there are any?

We probably need to show them inside the same modal, right? But how do we call it to show up again?

The good news is that validation errors would show by default, as in default Login form by Laravel. So, in fact, forcing the modal to appear is our only task here.

So, we will write a jQuery block for this.

First, at the bottom of resources/views/layouts/app.blade.php, let’s add a @yield directive:

    ...
    @yield('scripts')
</body>
</html>

Next, in resources/views/partials/login.blade at the bottom we add this:

@section('scripts')
@parent

@if($errors->has('email') || $errors->has('password'))
    <script>
    $(function() {
        $('#loginModal').modal({
            show: true
        });
    });
    </script>
@endif
@endsection

Yes, you can add @if Blade logic inside of scripts section. And $errors come by default from Laravel Auth validation, so we don’t need to pass them from anywhere.

Visual result is this:


Step 3. Register Form: Bootstrap Modal Popup and Link

This will be almost identical to Step 1, just with bigger form – more fields than login.

resources/views/partials/register.blade.php:

<div class="modal fade" id="registerModal" tabindex="-1" role="dialog" aria-labelledby="registerModal" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="registerModal">{{ __('Register') }}</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">×</span>
                </button>
            </div>
            <div class="modal-body">
                <form method="POST" id="registerForm">
                    @csrf

                    <div class="form-group row">
                        <label for="nameInput" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label>

                        <div class="col-md-6">
                            <input id="nameInput" type="text" class="form-control" name="name" value="{{ old('name') }}"  autocomplete="name" autofocus>

                            <span class="invalid-feedback" role="alert" id="nameError">
                                <strong></strong>
                            </span>
                        </div>
                    </div>

                    <div class="form-group row">
                        <label for="emailInput" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                        <div class="col-md-6">
                            <input id="emailInput" type="email" class="form-control" name="email" value="{{ old('email') }}" required autocomplete="email">

                            <span class="invalid-feedback" role="alert" id="emailError">
                                <strong></strong>
                            </span>
                        </div>
                    </div>

                    <div class="form-group row">
                        <label for="passwordInput" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

                        <div class="col-md-6">
                            <input id="passwordInput" type="password" class="form-control" name="password" required autocomplete="new-password">

                            <span class="invalid-feedback" role="alert" id="passwordError">
                                <strong></strong>
                            </span>
                        </div>
                    </div>

                    <div class="form-group row">
                        <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>

                        <div class="col-md-6">
                            <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
                        </div>
                    </div>

                    <div class="form-group row mb-0">
                        <div class="col-md-6 offset-md-4">
                            <button type="submit" class="btn btn-primary">
                                {{ __('Register') }}
                            </button>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>

Then, inside resources/views/layouts/app.blade.php:

<a class="nav-link" 
    style="cursor: pointer"
    data-toggle="modal" 
    data-target="#registerModal">{{ __('Register') }}</a>

...

@include('partials.login')
@include('partials.register')

Visually, the result is this:


Step 4. Register AJAX Submit and Validation Errors

In this case, different from Login form, we will not submit registration form to the same action. Well, we will, but only via AJAX.

Not sure if you know, but Laravel login/registration controllers will return you JSON validation errors if you ask them too. Yes, with proper correct validation code 422.

So, at the bottom of resources/views/partials/register.blade.php we add this JavaScript:

@section('scripts')
@parent

<script>
$(function () {
    $('#registerForm').submit(function (e) {
        e.preventDefault();
        let formData = $(this).serializeArray();
        $(".invalid-feedback").children("strong").text("");
        $("#registerForm input").removeClass("is-invalid");
        $.ajax({
            method: "POST",
            headers: {
                Accept: "application/json"
            },
            url: "{{ route('register') }}",
            data: formData,
            success: () => window.location.assign("{{ route('home') }}"),
            error: (response) => {
                if(response.status === 422) {
                    let errors = response.responseJSON.errors;
                    Object.keys(errors).forEach(function (key) {
                        $("#" + key + "Input").addClass("is-invalid");
                        $("#" + key + "Error").children("strong").text(errors[key][0]);
                    });
                } else {
                    window.location.reload();
                }
            }
        })
    });
})
</script>
@endsection

I won’t explain this JavaScript in detail, it’s pretty readable. The most important part is to send this: headers: { Accept: “application/json” }.

And that’s it, any validation errors will come almost immediately to the form, without refreshing the page:

And, that’s it!
Full repository for this project: LaravelDaily/Laravel-Login-Register-Modal-Bootstrap