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>
Source: Laravel — Components and Component Methods
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>
Source: Laravel — Anonymous Components and Component Attributes
$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>
Source: Laravel — Slots, Index Components and Dynamic Components
What the output shows:
-
class="featured"was appended tobook-card, anddata-isbnpassed through$attributes;compact(a prop, so not an attribute) hid the author line. -
The
titleslot’sclass="big"was merged with the component’salert-title. -
<x-card>resolved tocomponents/card/index.blade.phpand<x-card.header>tocard/header.blade.php: an index component groups a component and its parts in one directory. -
<x-dynamic-component :component="$widget">rendered thebuttoncomponent chosen at run time. -
:disabled="true"rendereddisabled="disabled", andfalseornullwould have omitted the attribute. -
$attributesvalues are escaped;{{ $attributes }}printsonclick="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:
-
@awarereads only what the caller wrote on the parent tag. Withcolor="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@propsdefault is not passed down. Giving both the same default, or always passing the attribute, keeps them in step. -
::classis 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 withsyntax error, unexpected token "{". With::, Blade drops one colon and passes the value on exactly as written, for the browser to evaluate. -
$componentin 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,withAttributesandwithNamecannot 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 |
|
|
Passing data |
Props and named slots, explicit and checked |
Sections and |
Defaults |
|
|
Nesting |
A layout component can use another ( |
|
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="&laquo; Previous"
1 aria-label="Go to page 1"
1 aria-label="Go to page 3"
1 aria-label="Next &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 « Previous and Next », 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
Source: Laravel — Testing Views
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().