Laravel 13 Blade Components: Props, Slots, and Reusable Templates
The same button, the same card, and the same alert copy-pasted across ten views is the most common template smell in Laravel apps. Blade components capture that markup once: props for data, slots for content, and $attributes for flexibility.
The Problem: Repeated Markup in Every View
The Copy-Pasted HTML Template Smell
We have all done it: the button on the homepage, the one on the product page, and the one in the admin panel are the same HTML copied three times with small class tweaks. The app works, but the day the design changes the button color you have to open ten views and edit the same line in each one. That is the classic template smell: duplicated markup that breaks consistency and turns every style change into a bug hunt.
What a Blade Component Is and What It Solves (DRY and Consistency)
A Blade component is a reusable UI fragment defined once and invoked as its own tag: <x-button>, <x-card>, or <x-post-card>. It solves exactly the problem above: you apply DRY to your markup, guarantee the button looks the same everywhere, and when the design changes you touch a single file. Laravel 13 (released on March 17, 2026, with a minimal upgrade path from Laravel 12) supports both approaches: anonymous components, a simple view in resources/views/components ideal for pure UI, and class-based components, a PHP class with logic, constructor, and a render() method.
Anonymous Components: The Quick Path
Creating x-button in resources/views/components
Anonymous components are the fastest way to start: any view inside resources/views/components automatically becomes a component with the x- prefix. Create the file button.blade.php and you have an <x-button> ready to use across the app:
Read also
{{-- resources/views/components/button.blade.php --}}
@props(['variant' => 'primary'])
<button {{ $attributes->merge(['class' => 'btn btn-' . $variant]) }}>
{{ $slot }}
</button>Props with @props and Default Values
Props are the data a component receives from the view that uses it. The @props directive declares them and, in the process, assigns defaults: here variant defaults to primary when nothing is passed. The same component serves every variant:
<x-button>Save changes</x-button>
<x-button variant="danger">Delete</x-button>Subdirectories and Naming Conventions (x-ui.button)
When the components folder grows, subdirectories keep things organized: a component at resources/views/components/ui/button.blade.php is invoked as <x-ui.button>, with dots separating levels. The convention is kebab-case filenames and a consistent hierarchy so any developer can find a component without thinking.
Props, $attributes, and Attribute Merging
Declared Props vs Extra Attributes: How Data Travels
The golden rule: every attribute you write on the tag that is not declared as a prop lands in the $attributes bag. If you call <x-button id="btn-save" data-confirm="true">, id and data-confirm travel in $attributes and render wherever you print them. That lets you pass any HTML attribute, event, or class without touching the component.
$attributes->merge() and the Special Class Merge
Attribute merging is what makes components truly flexible. {{ $attributes->merge(['class' => 'btn btn-' . $variant]) }} combines the attributes coming from the caller with the component's defaults. The detail that trips almost everyone up: in the special case of class, the merge concatenates instead of overwriting. If the caller passes class="w-full" and the component contributes btn btn-primary, the result is btn btn-primary w-full, not one class replacing the other. For every other attribute, the caller's value wins.
$attributes->class() for Conditional Class Lists
For conditional classes there is $attributes->class([...]), available since Laravel 9: it takes an array where each class is included when its value is truthy. It is the clean way to say "add the active class only when it applies" without building strings by hand:
<div {{ $attributes->class(['card', 'card-active' => $active]) }}>
{{ $slot }}
</div>Slots: Flexible Content Inside a Component
The Default Slot {{ $slot }}
If props are the data, slots are the content: the HTML written between the opening and closing tags. The component prints it with {{ $slot }}, so the same <x-card> works for a news item, a product, or a welcome message.
Named Slots with x-slot:title and Their Attributes
When a component needs several content areas, named slots keep them separate. On the call site they are defined with <x-slot:title> (or <x-slot name="title">) and printed in the component view as {{ $title }}. An alert with a title and a body:
{{-- resources/views/components/alert.blade.php --}}
@props(['type' => 'info'])
<div class="alert alert-{{ $type }}" {{ $attributes }}>
<h3>{{ $title }}</h3>
{{ $slot }}
</div><x-alert type="success">
<x-slot:title>Published</x-slot:title>
The article is now live on the blog.
</x-alert>hasActualContent() to Detect Real Content
Since Laravel 11, $slot->hasActualContent() tells you whether the slot contains real content or only empty HTML. It is handy for rendering wrappers conditionally: for example, not painting an actions container when the actions slot is empty.
Class-Based Components: Logic Inside the Component
php artisan make:component and the Class + View Pattern
When a component needs PHP logic — querying data, computing classes, deciding what to show — you move to class-based components. Generate them with php artisan make:component Badge, which creates the class in app/View/Components/Badge.php and the view resources/views/components/badge.blade.php.
Props via Public Properties and the Constructor
Props are declared as public properties and received through the constructor, with defaults for optional ones:
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class Badge extends Component
{
public function __construct(
public string $status,
public bool $withDot = false,
) {}
public function render()
{
return view('components.badge');
}
}render() with Inline Views and Accessing $component
The render() method returns the view, but it can also return a string with an inline view. Inside render() you can access the component itself through $component->name (the component's name), $component->attributes (the received attributes), and $component->slot (the slot content) — documented in the official 13.x docs — which lets you decide the view based on the input data.
public function render()
{
if ($this->status === 'published') {
return '<span {{ $attributes }} class="badge badge-green">{{ $slot }}</span>';
}
return view('components.badge');
}Layout as a Component and Dynamic Components
x-app-layout and Layout Slots (The Starter Kit Pattern)
The recommended pattern for new projects is modeling the layout as a component with slots — the approach the starter kits themselves use (Breeze and Jetstream) instead of classic inheritance with @extends and @section:
<x-app-layout>
<x-slot name="header">
<h1>Admin dashboard</h1>
</x-slot>
<div class="py-12">
<x-post-card :post="$post" />
</div>
</x-app-layout>The layout view defines {{ $header }} and {{ $slot }} in their positions, and pages only worry about content instead of repeating the document structure.
x-dynamic-component for Configurable Components
Dynamic components render at runtime: <x-dynamic-component :component="$blockType" /> renders whichever component the variable holds. It is the perfect piece for data-driven UI — lists of blocks stored in the database or in config — where you do not know in advance which component will render.
Real Example: Components on a Laravel 13 Blog
post-card Reused on the Homepage and Related Articles
A blog built with Laravel 13 is the perfect use case. The x-post-card component receives the Post via prop — using the Eloquent published query scope — and is reused on the homepage, in the related articles list, and in any section that lists entries. Change the card design once and the whole site updates.
An alert with Variants and a Class-Based Badge with Logic
In the admin panel, an x-alert with variants (info, success, danger) reports the outcome of actions, and the class-based x-badge shows each post's status (published or draft), deciding the color from PHP logic rather than from the view.
The View Before and After
The before: twenty lines of repeated markup in every view, with inline classes and copied conditions. The after: three lines with components that receive only the data that changes. Maintenance goes from "edit in ten places" to "edit one component."
Testing Components
renderComponent()->assertSee() and Testing Props, Slots, and Classes
Components are tested like any other view with Pest or PHPUnit. renderComponent() renders the component and its assertions check props, slots, attributes, and generated classes:
it('renders the button with the right variant', function () {
$this->renderComponent(Button::class, ['variant' => 'danger'])
->assertSee('btn-danger')
->assertSee('Delete');
});That way, a design change that breaks a class is caught by the suite before it reaches production.
Conclusion
Blade components turn duplicated markup into a library of pieces: anonymous for pure UI, class-based when there is logic, $attributes for flexibility, slots for content, and the layout-as-component pattern for structure. Start with the component you repeat the most in your app — probably a button or a card — and let the rest of the system grow from there. To keep building on these foundations, the blog has guides on Eloquent query scopes, asset builds with Vite, and interactivity with HTMX to round out a Laravel 13 app workflow.


