Build modern authentication screens with Laravel 8 and Bootstrap 5

Build modern authentication screens with Laravel 8 and  Bootstrap 5

I am Laravel and Bootstrap fan since the beginning. Even though recently Laravel switch to Tailwind CSS as its primary way of building interfaces, I still prefer Bootstrap for several reasons.

  • I like my HTML and CSS separated
  • Building or maintaining components and pages is much easier
  • Basic customization is made using Sass which makes global customization less time consuming
  • You can always use Bootstrap utilities to extend components directly into your HTML or add new ones just like Tailwind

In this tutorial, I will show you how to get started fast with Laravel 8 and Bootstrap 5. These are the things we'll cover:

  • Installation
  • Laravel Mix setup
  • Authentication scaffolding
  • Login, Register, and Recover design
  • User management

Also, I am going to use a pretty cool design system built with Bootstrap 5 to extend the default look and feel and add new components and utility classes for allowing me to build more complex architectures.

If you want your application's UI to look like the one in the article's image, then you should give Webpixels CSS a try.

Installation

I won't go into detail and write about the requirements and steps you need to take before installing Laravel. This information already exists in the official documentation

Now, if you're ready, let's dive in.

Create a new folder called laravel-starter-kit and install Laravel using your preferred method from the docs.

Next, we will install everything we need for our app.

Authentication Scaffolding

Laravel provides a basic starting point using Bootstrap, React, and/or Vue that will be helpful for many applications.

By default, Laravel uses NPM to install both of these frontend packages.

composer require laravel/ui

Generate basic scaffolding...

php artisan ui bootstrap

Generate login / registration scaffolding...

php artisan ui bootstrap --auth

After installing the laravel/ui Composer package and generating the frontend scaffolding, the package.json file will include the Bootstrap and Webpixels CSS packages to help you get started prototyping your application's frontend without worrying about the design.

Before compiling CSS, open package.json and replace devDependencies like in the example below. This way you'll make sure to use the latest Bootstrap version.

"devDependencies": {
   "@popperjs/core": "^2.6.0",
   "@webpixels/css": "^1.0.1",
   "axios": "^0.21",
   "bootstrap": "^5.0.1",
   "laravel-mix": "^6.0.6",
   "lodash": "^4.17.19",
   "postcss": "^8.2.2",
   "sass": "^1.32.11",
   "sass-loader": "^11.0.1"
}

Next, install your project's frontend dependencies using the Node package manager (NPM):

npm install

Configure Laravel Mix

Laravel Mix provides a clean, expressive API over compiling SASS, which are extensions of plain CSS that add variables, mixins, and other powerful features that make working with CSS much more enjoyable.

To configure Laravel Mix use the webpack.mix.js file. It comes with a default initial setup, but we'll make some improvements here:

let mix = require('laravel-mix');

mix.js('resources/js/app.js', 'public/js')
   .sass('resources/sass/app.scss', 'public/css',  {
      sassOptions: {
         includePaths: ['node_modules'],
      },
   });

mix.browserSync({
   proxy: 'localhost:8000',
   notify: true
});

if (mix.inProduction()) {
   mix.version();
   mix.sourceMaps();
}

To run your Mix tasks you only need to execute one of the NPM scripts that are included in the default Laravel package.json file. When you run the dev or production scripts, all of your application's CSS and JavaScript assets will be compiled and placed in your application's public directory. We will use these later, but just to be aware, here are the commands:

// Run all Mix tasks...
npm run dev

// Run all Mix tasks and minify output...
npm run prod

The frontend scaffolding is now ready. In the following steps, we will set up the database connection and run the migrations which will allow us to create some beautiful authentication screens.

Add the CSS styles

As I previously mentioned, we'll use Webpixels CSS to style our dashboard. It is a utility and component-centric Design System based on Bootstrap for fast, responsive UI development.

Open the resource/sass/app.scss file and replace its content with the following lines:

@import "@webpixels/css/base";
@import "@webpixels/css/forms";
@import "@webpixels/css/components";
@import "@webpixels/css/utilities";

Webpixels CSS will automatically load the latest version of Bootstrap 5. To learn more about how to customize it read the documentation.

Add Bootstrap to your app's JS

Open the resource/js/app.js file and replace its content with the following lines:

require('bootstrap');

Since now we are using Bootstrap 5, you can remove the bootstrap.js file located in the same folder. That one is added when laravel/ui is installed to include jQuery and Popper, which is not the case anymore.


Great! On the front end side, we're all set. Next, we will focus on setting up the database and build our app's backend.

Database setup

When you create a new Laravel project, the installation process automatically creates a .env file for configuration and credentials. Depending on your setup, you’ll need to modify the following block of settings to match your database configuration:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

With your connection set up, let's run the migrations using the dedicated artisan command:

php artisan migrate

Let's build our app

These are the final steps before starting our app:

Generate the app's key

Since this is your first time starting this project, you must set the key configuration option in your config/app.php configuration file:

php artisan key:generate

Run database migrations

Before running the migrations, you may need to manually configure the default string length generated by migrations in order for MySQL to create indexes for them. You may configure the default string length by calling the Schema::defaultStringLength method within the boot method of your App\Providers\AppServiceProvider class:

use Illuminate\Support\Facades\Schema;

/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
    Schema::defaultStringLength(191);
}

To run all of your outstanding migrations, execute the migrate Artisan command:

php artisan migrate

Start the app

After the application has been created, you may start Laravel's local development server using the Artisan CLI's serve command:

php artisan serve

The database is all set so we will take care now of the user interface.

Set up the routes

By default, Laravel comes with a couple of implemented routes used for displaying the Home and Welcome pages of the website. It also creates the authentication routes automatically when you install Laravel UI.

Next, we'll make some minor changes for displaying the Home page. Copy this in your routes/web.php file.

Auth::routes();

Route::get('/', [App\Http\Controllers\HomeController::class, 'index']);
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');

Create the layout

For creating the authentication pages and forms we will use Laravel's class-based components. Let's create a new guest layout using the make:component command which will place it in the App\View\Components directory:

php artisan make:component GuestLayout

The make:component command will also create a view template for the component. The view will be placed in the resources/views/components directory.

Once the layout component has been defined, place this content inside it:

<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Fonts -->
    <link rel="dns-prefetch" href="//fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css?family=Inter" rel="stylesheet">

    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div id="app">
        <main class="py-4">
            {{ $slot }}
        </main>
    </div>

    <!-- Scripts -->
    <script src="{{ asset('js/app.js') }}" defer></script>

</body>
</html>

As you can see, we are doing a few things here:

  • added the Inter font
  • linked the CSS styles
  • added the $slot variable within our layout component used for injecting the content from other pages
  • linked the app's scripts

Now we are ready to build the authentication pages :)

Create the authentication screens

For this part, I will use one of the free and beautiful pre-made authentication screens from the UI components library provided by Webpixels. They're all crafted with the latest version of Bootstrap (currently v5) and you can use the HTML code to create all the pages you need.

In the following example, I took the HTML from Webpixels and replaced its elements with the ones needed by Laravel to become fully functional.


The login page

Copy and paste the code below in your login page located in the resources/views/auth/login.blade.php:

<x-guest-layout>
    <div class="px-5 py-5 p-lg-0 bg-surface-secondary">
        <div class="d-flex justify-content-center">
            <div class="col-lg-5 col-xl-4 p-12 p-xl-20 position-fixed start-0 top-0 h-screen overflow-y-hidden bg-primary d-none d-lg-flex flex-column">
                <!-- Logo -->
                <a class="d-block" href="#">
                    <img src="https://preview.webpixels.io/web/img/logos/clever-light.svg" class="h-10" alt="...">
                </a>
                <!-- Title -->
                <div class="mt-32 mb-20">
                    <h1 class="ls-tight font-bolder display-6 text-white mb-5">
                        Let’s build something amazing today.
                    </h1>
                    <p class="text-white-80">
                        Maybe some text here will help me see it better. Oh God. Oke, let’s do it then.
                    </p>
                </div>
                <!-- Circle -->
                <div class="w-56 h-56 bg-orange-500 rounded-circle position-absolute bottom-0 end-20 transform translate-y-1/3"></div>
            </div>
            <div class="col-12 col-md-9 col-lg-7 offset-lg-5 border-left-lg min-h-lg-screen d-flex flex-column justify-content-center py-lg-16 px-lg-20 position-relative">
                <div class="row">
                    <div class="col-lg-10 col-md-9 col-xl-6 mx-auto ms-xl-0">
                        <div class="mt-10 mt-lg-5 mb-6 d-flex align-items-center d-lg-block">
                            <span class="d-inline-block d-lg-block h1 mb-lg-6 me-3">👋</span>
                            <h1 class="ls-tight font-bolder h2">
                                {{ __('Nice to see you!') }}
                            </h1>
                        </div>
                        <form method="POST" action="{{ route('login') }}">
                            @csrf
                            <div class="mb-5">
                                <label class="form-label" for="email">{{ __('E-Mail Address') }}</label>
                                <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 class="mb-5">
                                <div class="d-flex align-items-center justify-content-between">
                                    <div>
                                        <label class="form-label" for="password">{{ __('Password') }}</label>
                                    </div>
                                    <div class="mb-2">
                                        @if (Route::has('password.request'))
                                        <a href="{{ route('password.request') }}" class="small text-muted">Forgot password?</a>
                                        @endif
                                    </div>
                                </div>
                                <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 class="mb-5">
                                <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>
                                <button type="submit" class="btn btn-primary w-full">
                                    {{ __('Sign in') }}
                                </button>
                            </div>
                        </form>
                        <div class="my-6">
                            <small>{{ __('Don\'t have an account') }}</small>
                            <a href="{{ route('register') }}" class="text-warning text-sm font-semibold">{{ __('Sign up') }}</a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</x-guest-layout>

The register page

Copy and paste the code below in your register page located in the resources/views/auth/register.blade.php:

<x-guest-layout>
    <div class="px-5 py-5 p-lg-0 bg-surface-secondary">
        <div class="d-flex justify-content-center">
            <div class="col-lg-5 col-xl-4 p-12 p-xl-20 position-fixed start-0 top-0 h-screen overflow-y-hidden bg-primary d-none d-lg-flex flex-column">
                <!-- Logo -->
                <a class="d-block" href="#">
                    <img src="https://preview.webpixels.io/web/img/logos/clever-light.svg" class="h-10" alt="...">
                </a>
                <!-- Title -->
                <div class="mt-32 mb-20">
                    <h1 class="ls-tight font-bolder display-6 text-white mb-5">
                        Let’s build something amazing today.
                    </h1>
                    <p class="text-white-80">
                        Maybe some text here will help me see it better. Oh God. Oke, let’s do it then.
                    </p>
                </div>
                <!-- Circle -->
                <div class="w-56 h-56 bg-orange-500 rounded-circle position-absolute bottom-0 end-20 transform translate-y-1/3"></div>
            </div>
            <div class="col-12 col-md-9 col-lg-7 offset-lg-5 border-left-lg min-h-lg-screen d-flex flex-column justify-content-center py-lg-16 px-lg-20 position-relative">
                <div class="row">
                    <div class="col-lg-10 col-md-9 col-xl-6 mx-auto ms-xl-0">
                        <div class="mt-10 mt-lg-5 mb-6 d-flex align-items-center d-lg-block">
                            <span class="d-inline-block d-lg-block h1 mb-lg-6 me-3">👋</span>
                            <h1 class="ls-tight font-bolder h2">
                                {{ __('Create an account') }}
                            </h1>
                        </div>
                        <form method="POST" action="{{ route('register') }}">
                            @csrf
                            <div class="mb-5">
                                <label class="form-label" for="email">{{ __('Name') }}</label>
                                <input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name') }}" required autocomplete="name" autofocus>

                                @error('name')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                            <div class="mb-5">
                                <label class="form-label" for="email">{{ __('E-Mail Address') }}</label>
                                <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 class="mb-5">
                                <label class="form-label" for="password">{{ __('Password') }}</label>
                                <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 class="mb-5">
                                <label class="form-label" for="password-confirm">{{ __('Confirm Password') }}</label>
                                <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
                            </div>
                            <div>
                                <button type="submit" class="btn btn-primary w-full">
                                    {{ __('Sign up') }}
                                </button>
                            </div>
                        </form>
                        <div class="my-6">
                            <small>{{ __('Already have an account?') }}</small>
                            <a href="{{ route('login') }}" class="text-warning text-sm font-semibold">{{ __('Sign in') }}</a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</x-guest-layout>

Other authentication screens

Inside your Laravel app, you will notice there are additional pages for account verification, password reset, and more. I took care of those too and you can find them in the project's repo.

For those who want to explore the code, here's the GitHub repo for this project.

Coming up

This is the first part of this series. In order to keep it clean and simple, I will dedicate one article for each major step towards our app completion.

In the following parts we will take it a step further and prepare your app for more complex pages and features, like:

  • user list and management
  • user roles and permissions
  • simple blog system

Follow me and subscribe to the newsletter to be the first to find out when I release them.