Blade Components and Layouts

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Laravel 13.x (13.34.0) on PHP 8.3 to 8.5, checked on 2026-10-05. Every component is rendered in the Bookshelf application used across this guide. A component is a reusable piece of markup used as an HTML-like tag, <x-book-card>, with props, attributes and slots. It builds on the directives of Blade Templates, and Livewire components (Livewire) extend the same syntax with server-side state.

Class components

php artisan make:component BookCard creates app/View/Components/BookCard.php and resources/views/components/book-card.blade.php. The constructor’s parameters are the props; public properties and public methods are available in the view:

<?php
// app/View/Components/BookCard.php
namespace App\View\Components;

use App\Models\Book;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;

/** A class component: constructor arguments are its props, public members reach the view. */
class BookCard extends Component
{
    public function __construct(
        public Book $book,
        public bool $compact = false,
    ) {}

    public function age(): string                                   // callable as {{ $age() }}
    {
        return $this->book->published_year ? (now()->year - $this->book->published_year).' years old' : 'undated';
    }

    public function shouldRender(): bool                            // render nothing for drafts
    {
        return $this->book->exists;
    }

    public function render(): View
    {
        return view('components.book-card');
    }
}
{{-- resources/views/components/book-card.blade.php --}}
<article {{ $attributes->merge(['class' => 'book-card']) }}>
    <h2>{{ $book->title }}</h2>
    @unless ($compact)
        <p>{{ $book->author->name }} · {{ $age() }}</p>
    @endunless
    {{ $slot }}
</article>

Props are passed as attributes: a plain attribute passes a string, a :-prefixed one passes a PHP expression, and :$book is short for :book="$book". Kebab-case attributes map to camelCase parameters (is-new to $isNew). Constructor dependencies that are not props, such as a service, are injected by the container when they are listed before the props. shouldRender() returning false renders nothing at all.

Anonymous components and attributes

A component with no logic needs no class: a view in resources/views/components is a component, with its props declared by @props. Everything passed that is not a prop lands in the $attributes bag:

{{-- resources/views/components/button.blade.php --}}
@props(['variant' => 'primary', 'type' => 'button'])

<button type="{{ $type }}" {{ $attributes->class(['btn', 'btn-'.$variant, 'btn-wide' => $attributes->has('wide')])->except('wide') }}>
    {{ $slot }}
</button>
{{-- resources/views/components/alert.blade.php --}}
@props(['type' => 'info'])

<div role="alert" {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
    @isset($title)
        <strong {{ $title->attributes->class('alert-title') }}>{{ $title }}</strong>
    @endisset
    {{ $slot }}
    @if ($actions->isNotEmpty())
        <footer>{{ $actions }}</footer>
    @endif
</div>

$attributes->merge(['class' => ...]) appends the caller’s classes to the defaults, but for any other attribute the caller’s value replaces the default. class([...]) takes conditional classes like @class; only(), except(), has(), get(), whereStartsWith('wire:model') and prepends() filter and adjust the bag. The except('wide') above removes a flag attribute that only drives a class.

Slots

{{ $slot }} prints the content between the tags. Named slots are passed with <x-slot:name> and arrive as variables; each slot has its own attributes and isEmpty()/isNotEmpty(). A page that uses all of the above:

{{-- resources/views/shelf/components.blade.php --}}
<x-layouts.app title="Components">
    <x-slot:head>
        <meta name="robots" content="noindex">
    </x-slot>

    <x-book-card :book="$book" class="featured" data-isbn="{{ $book->isbn }}">
        <x-button variant="secondary" wide onclick="borrow()">Borrow</x-button>
    </x-book-card>

    <x-book-card :$book compact />

    <x-alert type="warning" id="due">
        <x-slot:title class="big">Due soon</x-slot>
        Return by Friday.
        <x-slot:actions><a href="/loans">Renew</a></x-slot>
    </x-alert>

    <x-card>
        <x-card.header>Statistics</x-card.header>
        {{ $count }} books
    </x-card>

    <x-dynamic-component :component="$widget" type="submit">Save</x-dynamic-component>

    <x-button :type="'re' . 'set'" class="extra" :disabled="true">Clear</x-button>
</x-layouts.app>
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Components - Bookshelf</title>
    <meta name="robots" content="noindex">
</head>
<body>
    <nav>Bookshelf</nav>
    <main><article class="book-card featured" data-isbn="978-0-8070-8305-4">
    <h2>Kindred</h2>
            <p>Octavia E. Butler · 47 years old</p>
        <button type="button" class="btn btn-secondary btn-wide" onclick="borrow()">
    Borrow
</button>
</article>
    <article class="book-card">
    <h2>Kindred</h2>
</article>
    <div role="alert" class="alert alert-warning" id="due">
            <strong class="alert-title big">Due soon</strong>
        Return by Friday.
            <footer><a href="/loans">Renew</a></footer>
    </div>
    <section class="card"><header class="card-header">Statistics</header>
        26 books</section>
    <button type="submit" class="btn btn-primary">
    Save
</button>
    <button type="reset" class="btn btn-primary extra" disabled="disabled">
    Clear
</button></main>
</body>
</html>

What the output shows:

  • class="featured" was appended to book-card, and data-isbn passed through $attributes; compact (a prop, so not an attribute) hid the author line.

  • The title slot’s class="big" was merged with the component’s alert-title.

  • <x-card> resolved to components/card/index.blade.php and <x-card.header> to card/header.blade.php: an index component groups a component and its parts in one directory.

  • <x-dynamic-component :component="$widget"> rendered the button component chosen at run time.

  • :disabled="true" rendered disabled="disabled", and false or null would have omitted the attribute.

  • $attributes values are escaped; {{ $attributes }} prints onclick="borrow()" as given, so attributes from user input must never be spread into a component unfiltered.

Parent data, Alpine attributes and scoped slots

Three features cover what a component needs from outside its own tag. A child component does not see its parent’s props, so a menu item cannot know the menu’s color. @aware declares which parent props it reads, with a default:

{{-- resources/views/components/shelf-menu/index.blade.php --}}
@props(['color' => 'stone'])

<ul {{ $attributes->merge(['class' => 'menu bg-'.$color.'-100']) }}>
    {{ $slot }}
</ul>

{{-- resources/views/components/shelf-menu/item.blade.php --}}
@aware(['color' => 'gray'])
@props(['href'])

<li><a href="{{ $href }}" {{ $attributes->class(['text-'.$color.'-800']) }}>{{ $slot }}</a></li>

The test renders the menu twice, passes Alpine.js attributes to the button component shown above, and reaches a class component’s method from inside its slot:

<?php
// tests/Feature/ComponentScopeTest.php
use App\Models\Book;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Blade;

uses(RefreshDatabase::class);

it('shares explicitly passed parent props with @aware', function () {
    $html = Blade::render(<<<'BLADE'
        <x-shelf-menu color="amber">
            <x-shelf-menu.item href="/books">Books</x-shelf-menu.item>
        </x-shelf-menu>
        <x-shelf-menu>
            <x-shelf-menu.item href="/loans">Loans</x-shelf-menu.item>
        </x-shelf-menu>
        BLADE);

    expect($html)->toContain('<ul class="menu bg-amber-100">')
        ->toContain('<a href="/books" class="text-amber-800">Books</a>')
        ->toContain('<ul class="menu bg-stone-100">')              // the parent's default ...
        ->toContain('<a href="/loans" class="text-gray-800">Loans</a>');  // ... is not visible to @aware
});

it('passes Alpine attributes through with ::', function () {
    $html = Blade::render('<x-button ::class="{ \'btn-busy\': saving }" x-on:click="saving = true">Save</x-button>');

    expect($html)->toContain(':class="{ \'btn-busy\': saving }"');
});

it('reaches the component from its slot with $component', function () {
    $book = Book::factory()->create(['title' => 'Dawn', 'published_year' => 1987]);
    $html = Blade::render('<x-book-card :$book compact><p>{{ $component->age() }}, {{ $component->book->title }}</p></x-book-card>', ['book' => $book]);

    expect($html)->toContain('<p>'.(now()->year - 1987).' years old, Dawn</p>');
});
<ul class="menu bg-amber-100">
    <li><a href="/books" class="text-amber-800">Books</a></li>
</ul>
<ul class="menu bg-stone-100">
    <li><a href="/loans" class="text-gray-800">Loans</a></li>
</ul>
<button type="button" class="btn btn-primary" :class="{ 'btn-busy': saving }" x-on:click="saving = true">
    Save
</button>
<article class="book-card">
    <h2>Dawn</h2>
        <p>39 years old, Dawn</p>
</article>

The three tests pass, and the HTML above is what each Blade::render() returned:

  • @aware reads only what the caller wrote on the parent tag. With color="amber" the item followed the menu. The second menu used its own default (stone), but the item fell back to its default (gray): a parent’s @props default is not passed down. Giving both the same default, or always passing the attribute, keeps them in step.

  • ::class is how Alpine.js and Livewire bindings reach the HTML. Blade treats a :-prefixed attribute on a component as PHP, so :class="{ 'btn-busy': saving }" failed to compile with syntax error, unexpected token "{". With ::, Blade drops one colon and passes the value on exactly as written, for the browser to evaluate.

  • $component in a slot is the component instance, so the slot can call its public methods and read its public properties (age(), book), which is what other frameworks call a scoped slot. Because public members are part of a component’s interface this way, Blade reserves a few names for itself: data, render, resolve, resolveView, shouldRender, view, withAttributes and withName cannot be public properties or methods of a component, as the reserved keywords list says.

Layouts: components or inheritance

The page above used a layout component: <x-layouts.app> is the anonymous component resources/views/components/layouts/app.blade.php, with the page as its slot and title as a prop. This is the form the starter kits use:

{{-- resources/views/components/layouts/app.blade.php --}}
@props(['title' => null])

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>{{ $title ? $title.' - ' : '' }}Bookshelf</title>
    {{ $head ?? '' }}
</head>
<body>
    <nav>Bookshelf</nav>
    <main>{{ $slot }}</main>
</body>
</html>
Component layout Template inheritance

Page syntax

<x-layouts.app title="..."> around the content

@extends('layouts.site') and @section('content')

Passing data

Props and named slots, explicit and checked

Sections and @yield; variables leak from page to layout

Defaults

{{ $head ?? '' }}, @isset($slot)

@section ... @show with @parent

Nesting

A layout component can use another (<x-layouts.app> inside <x-layouts.admin>)

@extends chains

Used by

Starter kits, Livewire full-page components

Older applications, the shelf page on Blade Templates

Both compile to the same kind of PHP and can be mixed in one application. Components make the layout’s inputs explicit, which matters as the number of layouts grows.

Component namespaces and packages

Components in resources/views/components and classes in app/View/Components are discovered automatically. Other locations are registered in a service provider:

<?php
// AppServiceProvider::boot() or a package's service provider (from the documentation; not run here)
use Illuminate\Support\Facades\Blade;

Blade::anonymousComponentPath(resource_path('views/catalogue-ui'), 'ui');   // <x-ui::badge>
Blade::componentNamespace('Nightshade\\Views\\Components', 'nightshade');     // <x-nightshade::calendar>
Blade::component('book-badge', \App\View\Components\Badge::class);            // an explicit alias

vendor::name keeps a package’s components from colliding with the application’s; Flux UI uses its own flux: tag prefix (<flux:button>). Writing packages is on Package Development and Ecosystem.

Pagination views

{{ $books->links() }} renders the paginator with a Tailwind view by default. Paginator::useBootstrapFive() (or useBootstrapFour()) in AppServiceProvider::boot() switches to Bootstrap markup, and php artisan vendor:publish --tag=laravel-pagination copies the views (tailwind, simple-tailwind, bootstrap-3 to -5, semantic-ui) into resources/views/vendor/pagination for editing. The markup on page 2 of the book list:

$ curl -s 'localhost:8000/books?page=2' | grep -o '<nav[^>]*>\|aria-label="[^"]*"\|rel="[a-z]*"\|aria-current="page"' | sort | uniq -c
      1 <nav role="navigation" aria-label="Pagination Navigation">
      1 aria-current="page"
      1 aria-label="&amp;laquo; Previous"
      1 aria-label="Go to page 1"
      1 aria-label="Go to page 3"
      1 aria-label="Next &amp;raquo;"
      2 rel="next"
      2 rel="prev"

The navigation is accessible (aria-current, labels per page), with one flaw: the previous and next labels come from the translation strings &laquo; Previous and Next &raquo;, which {{ }} escapes, so a screen reader announces the entity text. Publishing lang/en/pagination.php (php artisan lang:publish) with plain Previous and Next fixes it. Pagination itself (paginate, simplePaginate, cursorPaginate) is on Collections and Pagination.

Testing components

$this->blade() renders a template string with components, which tests props, attributes, slots and shouldRender() together:

<?php
// tests/Feature/ComponentsTest.php
use App\Models\Book;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

it('merges classes and keeps props out of the attributes', function () {
    $this->blade('<x-button variant="danger" wide class="mt-2">Delete</x-button>')
        ->assertSee('class="btn btn-danger btn-wide mt-2"', escape: false)
        ->assertDontSee('variant=', escape: false)
        ->assertDontSee(' wide', escape: false);
});

it('renders a class component and its method', function () {
    $book = Book::factory()->create(['title' => 'Dawn', 'published_year' => 1987]);

    $this->blade('<x-book-card :book="$book" />', ['book' => $book])
        ->assertSee('Dawn')
        ->assertSee((now()->year - 1987).' years old');
});

it('renders nothing when shouldRender() is false', function () {
    $this->blade('<x-book-card :book="$book" />', ['book' => new Book(['title' => 'Draft'])])
        ->assertDontSee('Draft');
});
$ php artisan test --filter=ComponentsTest
   PASS  Tests\Feature\ComponentsTest
  ✓ it merges classes and keeps props out of the attributes              0.17s
  ✓ it renders a class component and its method                          0.03s
  ✓ it renders nothing when shouldRender() is false                      0.01s

  Tests:    3 passed (6 assertions)
  Duration: 0.28s

The first version of these tests used the documented $this->component(BookCard::class, [...]) helper and failed twice: it renders the component’s view directly, so $slot is undefined ("Undefined variable $slot") and shouldRender() is not consulted (the draft book was rendered and failed on its missing author). component() suits components without slots; blade() exercises the real tag.

Where older books and tutorials are outdated

  • @component('alert') ... @slot('title') ... @endcomponent (Stauffer ch. 4) is the Laravel 5.4 syntax; tag components (<x-alert>, <x-slot:title>) arrived in Laravel 7 and are the norm.

  • <x-slot name="title"> still works, but <x-slot:title> (Laravel 9) is shorter and allows slot attributes.

  • Layouts only through @extends (Stauffer ch. 4) are now usually component layouts, which the starter kits generate.

  • Bootstrap as the default pagination markup (Laravel 5.x) changed to Tailwind in Laravel 8; Bootstrap is opt-in with useBootstrapFive().