Laravel Blade Templates: Components, Slots, CRUD Views and Breeze Auth

Arlind Musliu cofounder at Lucky Media
Arlind Musliu

January 13, 2024 · 7 min read

Laravel for Beginners: Blade and Breeze

2026 UPDATE - LARAVEL 13

We’re excited to announce that we have updated all of our blog post examples to reflect the new Laravel 13 version! Our previous examples were based on Laravel 10, but with the release of Laravel 13, we wanted to ensure that our readers have access to the most up-to-date information and examples.

Laravel Breeze and Blade

When combined with Laravel Breeze, you get a simple yet effective starting point for authentication views and a clear example of Blade in action. Let’s dive into the basics of Laravel Blade and how it’s used within Breeze to create a seamless user experience.

Why do we use Blade

What is Laravel Blade?

Blade is Laravel’s built-in templating engine, designed to provide developers with a convenient way to write HTML templates while also allowing the use of PHP. It offers an expressive syntax for extending layouts, displaying data, and constructing reusable components.

Blade Syntax and Directives

Blade templates use a mix of HTML and Blade directives – special tokens that Blade recognizes and converts into PHP code. Here are some of the fundamental directives:

  • @extends: Indicates that the template extends a layout.

  • @section and @endsection: Define a section of content.

  • @yield: Used in layouts to display the content of a section.

  • @include: Includes another Blade file within the template.

  • {{ }}: Echoes data, automatically escaping HTML entities for security.

  • @if, @elseif, @else, @endif: Control structures for conditional statements.

  • @foreach, @endforeach: Loops through a data array.

Blade Layouts

Blade allows you to create a master layout that serves as a template for your application’s look. Here’s a simple example of a layout:

<!DOCTYPE html>
<html lang="en">
<head>
    <!-- Head Contents -->
</head>
<body>
    <header>
        <!-- Header Content -->
    </header>

    <main>
        @yield('content')
    </main>

    <footer>
        <!-- Footer Content -->
    </footer>
</body>
</html>

Using @yield('content'), you define a placeholder for where the content of your child views will be injected.

Blade Templates with Breeze

Laravel Breeze simplifies authentication by providing a minimal and clean starting point, including Blade views for login, registration, password reset, and email verification. Let’s see how Blade and Breeze work together.

To install Breeze, require it via Composer and then run the install command:

composer require laravel/breeze --dev
php artisan breeze:install

As of Laravel 13, php artisan breeze:install prompts you to choose a frontend stack. You can also pass the stack name directly. The four available stacks are:

  • blade - simple Blade views, no JavaScript framework, good for beginners

  • react - React + Inertia.js, SPA experience

  • vue - Vue 3 + Inertia.js, SPA experience

  • livewire - Livewire v3, reactive UI without writing JavaScript

Laravel 13 Breeze includes passkey support out of the box. After installation, users can register and log in using Face ID, fingerprint, or a hardware security key.

After installing Breeze, you’ll notice that the views are stored in resources/views/auth. Here’s an example of how a login form might look using Blade:

@extends('layouts.app')

@section('content')
<div>
    <form method="POST" action="{{ route('login') }}">
        @csrf

        <!-- Email Input -->
        <div>
            <label for="email">Email</label>
            <input id="email" type="email" name="email" required autocomplete="email" autofocus>
        </div>

        <!-- Password Input -->
        <div>
            <label for="password">Password</label>
            <input id="password" type="password" name="password" required>
        </div>

        <!-- Submit Button -->
        <div>
            <button type="submit">Login</button>
        </div>
    </form>
</div>
@endsection

In this template, we’re extending the app layout and defining the content section. We use Blade’s @csrf directive to include a CSRF token field in the form for security, and {{ route('login') }} to generate the URL to the login route.

The screenshot below shows the Breeze Profile Edit page where we can modify the user data.

Laravel Breeze update profile

Blade Components

Blade also supports components, which are reusable pieces of the user interface. Breeze uses components to keep views DRY (Don’t Repeat Yourself). For example, you might have a component for an input field:

<input {{ $attributes }}>

You can use this component in your forms like so:

<x-input id="email" type="email" name="email" required />

Blog Post Views

Let’s create some of the views for our blog app example.

View for Displaying All Posts

First, we need to create a new folder named posts inside of the resources/views folder. Then we create a new file index.blade.php.

@extends('layouts.app')

@section('title', 'All Blog Posts')

@section('content')
    <h1>All Blog Posts</h1>
    <ul>
        @forelse ($posts as $post)
            <li>
                <a href="{{ route('posts.show', $post) }}">{{ $post->title }}</a>
                <p>{{ $post->created_at->toFormattedDateString() }}</p>
            </li>
        @empty
            <li>No posts available.</li>
        @endforelse
    </ul>
@endsection

In this view, we’re extending a main layout and then defining a section for the content. We loop through all the posts using @forelse, which also handles the case where there are no posts. Each post title is a link to the single post view.

View for Displaying a Single Post

We create another file in the same directory, show.blade.php:

@extends('layouts.app')

@section('title', $post->title)

@section('content')
    <article>
        <h1>{{ $post->title }}</h1>
        <p>Published {{ $post->created_at->diffForHumans() }}</p>
        <div>{{ $post->content }}</div>

        {{-- Check if there are any tags --}}
        @if($post->tags->isNotEmpty())
        <p>Tags:</p>
        <ul>
            @foreach($post->tags as $tag)
                <li>{{ $tag->name }}</li>
            @endforeach
        </ul>
        @endif
    </article>

    <a href="{{ route('posts.index') }}">Back to all posts</a>
@endsection

In the single post view, we show the post title, publication date, and content. The diffForHumans() will display the date as 5 days ago or 2 years ago depending on the time difference between the current moment and the post date.

The screenshot below displays the Single Post page of a randomly selected post that we tested.

single post show Laravel Blade

View for Creating a New Post

We create another file create.blade.php for displaying the form to create a new post.

@extends('layouts.app')

@section('title', 'Create New Post')

@section('content')
    <h1>Create New Post</h1>

    <form method="POST" action="{{ route('posts.store') }}">
        @csrf

        <div>
            <label for="title">Title</label>
            <input type="text" id="title" name="title" value="{{ old('title') }}" required>
            @error('title')
                <p>{{ $message }}</p>
            @enderror
        </div>

        <div>
            <label for="content">Content</label>
            <textarea id="content" name="content" required>{{ old('content') }}</textarea>
            @error('content')
                <p>{{ $message }}</p>
            @enderror
        </div>

 		 <div>
            <label for="tags">Tags</label>
            <select name="tags[]" id="tags" multiple>
                @foreach (\App\Models\Tag::all() as $tag)
                    <option value="{{ $tag->id }}" {{ in_array($tag->id, old('tags', [])) ? 'selected' : '' }}>{{ $tag->name }}</option>
                @endforeach
            </select>
        </div>

        <div>
            <button type="submit">Publish Post</button>
        </div>
    </form>
@endsection

In the create post view, we have a form with fields for the post title and content. We use the @csrf directive to protect against cross-site request forgery. The @error directive is used to display validation errors for each field. The old() function is used to repopulate the fields with the previously entered data in case of a validation error.

Also, change function of the store in PostController:

// Store a newly created blog post
    public function store(Request $request)
    {
        $validatedData = $request->validate([
            'title' => 'required|max:255',
            'content' => 'required',
        ]);

        $tags = $request->validate([
            'tags' => 'required|array',
            'tags.*' => 'exists:tags,id',
        ])['tags'];

        // the new post belongs to the authenticated user
        $post = auth()->user()->posts()->create($validatedData);

        // we add tags to the post by using the sync() method
        $post->tags()->sync($tags['tags']);

        // PostPublished::dispatch($post);

        return redirect()->route('posts.show', $post);
    }

Layout File

We will create a new folder in the views directory named as layouts. Then we will create a new file inside of that directory app.blade.php.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title') - My Blog</title>
</head>
<body>
    <header>
        <nav>
            <a href="{{ url('/') }}">Home</a>
            <a href="{{ route('posts.index') }}">Blog</a>
            <a href="{{ route('posts.create') }}">Write a Post</a>
        </nav>
    </header>

    <main>
        @yield('content')
    </main>

    <footer>
        <p>© My Blog</p>
    </footer>
</body>
</html>

The layout file provides a basic structure for the HTML pages, including a header with navigation links, a main content area where the individual views will be rendered, and a footer. The @yield directive is used to insert the content from the child views into the layout.

These Blade views provide the basic functionality for a blog to display posts and allow users to create new ones. You can customize the HTML and styling to match your blog’s design and requirements.

laravel best framework php

Anonymous Blade Components: The x- System

The x- component system (introduced in Laravel 7) is now the standard way to build reusable Blade UI. Anonymous components require no PHP class, just a Blade file in resources/views/components/.

Create resources/views/components/card.blade.php:

{{-- resources/views/components/card.blade.php --}}
<div class="border rounded-lg shadow p-4 bg-white">
    @if(isset($header))
        <div class="border-b pb-2 mb-4 font-bold text-lg">
            {{ $header }}
        </div>
    @endif

    <div>
        {{ $slot }}
    </div>
</div>

Use it anywhere with the <x-card> tag. The content between the opening and closing tags becomes $slot. Named slots use <x-slot>:

{{-- Using the card component with a named slot --}}
<x-card>
    <x-slot:header>Latest Posts</x-slot:header>

    @forelse ($posts as $post)
        <p>{{ $post->title }}</p>
    @empty
        <p>No posts yet.</p>
    @endforelse
</x-card>

Pass data as props using the colon syntax. Props declared in a @props directive become typed variables in the component:

{{-- resources/views/components/alert.blade.php --}}
@props(['type' => 'info', 'message'])

<div class="alert alert-{{ $type }}">
    {{ $message }}
</div>
{{-- Usage --}}
<x-alert type="success" message="Post created successfully!" />
<x-alert message="Something went wrong." type="error" />

@stack and @push for page-specific assets

Not every page needs the same JavaScript. @stack and @push let child views inject scripts or styles into named slots in the layout, without adding them to every page.

First, add a @stack placeholder in your layout before the closing </body>:

{{-- resources/views/layouts/app.blade.php --}}
<body>
    <main>
        @yield('content')
    </main>

    {{-- Page-specific scripts go here --}}
    @stack('scripts')
</body>

Then from any child view, push content into that stack:

{{-- resources/views/posts/create.blade.php --}}
@extends('layouts.app')

@section('content')
    {{-- form here --}}
@endsection

{{-- Only loads on the create post page --}}
@push('scripts')
    <script>
        // Initialize a date picker, character counter, etc.
        console.log('Create post page loaded');
    </script>
@endpush

Completing the Blog CRUD: Edit, Delete, and Flash Messages

Edit Post View

The edit view pre-populates form fields with the existing post data. Use old('field', $post->field) so that if validation fails, the form shows the user’s last input rather than the original value. Use @method('PUT') because HTML forms only support GET and POST:

{{-- resources/views/posts/edit.blade.php --}}
@extends('layouts.app')

@section('title', 'Edit: ' . $post->title)

@section('content')
    <h1>Edit Post</h1>

    <form method="POST" action="{{ route('posts.update', $post) }}">
        @csrf
        @method('PUT')

        <div>
            <label for="title">Title</label>
            <input
                type="text"
                id="title"
                name="title"
                value="{{ old('title', $post->title) }}"
                required
            >
            @error('title')
                <p class="text-red-500">{{ $message }}</p>
            @enderror
        </div>

        <div>
            <label for="content">Content</label>
            <textarea id="content" name="content" required>
                {{ old('content', $post->content) }}
            </textarea>
            @error('content')
                <p class="text-red-500">{{ $message }}</p>
            @enderror
        </div>

        <button type="submit">Update Post</button>
    </form>
@endsection

Delete Button

Deleting requires a POST form with @method('DELETE'). A small inline script adds a confirmation dialog so users do not accidentally delete posts:

{{-- Add to show.blade.php or index.blade.php --}}
@auth
    @if(auth()->id() === $post->user_id)
        <form
            method="POST"
            action="{{ route('posts.destroy', $post) }}"
            onsubmit="return confirm('Are you sure you want to delete this post?')"
        >
            @csrf
            @method('DELETE')
            <button type="submit" class="text-red-500 hover:underline">
                Delete Post
            </button>
        </form>
    @endif
@endauth

Flash Messages

Flash messages appear once after a redirect, perfect for "Post created successfully!" confirmations. Add the display code to your layout file so it works across all pages:

{{-- In resources/views/layouts/app.blade.php, inside <main> --}}
@if (session('success'))
    <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
        {{ session('success') }}
    </div>
@endif

@if (session('error'))
    <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
        {{ session('error') }}
    </div>
@endif

@yield('content')

Flash from any controller with:

return redirect()->route('posts.index')->with('success', 'Post created successfully!');
return redirect()->back()->with('error', 'Something went wrong.');

Auth directives: @auth, @guest, and @can

Breeze sets up authentication for you, but you control what shows in Blade based on auth state using these directives:

{{-- Show different nav items based on auth state --}}
<nav>
    <a href="{{ url('/') }}">Home</a>
    <a href="{{ route('posts.index') }}">Blog</a>

    @auth
        {{-- Only visible to logged-in users --}}
        <a href="{{ route('posts.create') }}">Write a Post</a>
        <a href="{{ route('profile.edit') }}">Profile</a>
        <form method="POST" action="{{ route('logout') }}">
            @csrf
            <button type="submit">Logout</button>
        </form>
    @endauth

    @guest
        {{-- Only visible to guests (not logged in) --}}
        <a href="{{ route('login') }}">Login</a>
        <a href="{{ route('register') }}">Register</a>
    @endguest
</nav>

Use @can when you have Laravel Policies set up and need finer-grained control (for example, only showing "Edit" to the post author):

@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">Edit Post</a>
@endcan

@can('delete', $post)
    {{-- delete form here --}}
@endcan

@cannot('create', App\Models\Post::class)
    <p>You do not have permission to create posts.</p>
@endcannot

Conclusion

Blade is a powerful templating engine that makes writing and managing your web application HTML a pleasure. Beyond layouts and basic directives, the x-component system gives you reusable UI building blocks, @stack and @push keep your JavaScript lean, and auth directives tie cleanly into Breeze authentication. With the full CRUD views covered here, you have everything you need to build a complete blog with Laravel and Blade.

Upcoming Articles in the Series

  1. Laravel for Beginners: Service Providers

  2. Laravel for Beginners: Form Requests and Validation Rules

  3. Laravel for Beginners: Task Scheduling for Automation

This article is part of our series Laravel for Beginners: A Step-by-Step Guide to Learn the Concepts.


Bring Your Ideas to Life 🚀

If you need help with a Laravel project let’s get in touch.

Lucky Media is proud to be recognized as a leading Laravel Development Agency

Technologies

Laravel
Arlind Musliu cofounder at Lucky Media
Arlind Musliu

Cofounder and CFO of Lucky Media

Stay up-to-date

Be updated with all news, products and tips we share!

Let’s chat

We partner with a limited number of brands each quarter to ensure senior-level attention on every project.

lokman and arlind headshots
Teamwork