Web Development 5-8 minutes

Laravel 13 Blade Components: Props, Slots, and Reusable Templates

Diego Cortés
Diego Cortés
Full Stack Developer & SEO Specialist
Share:
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:

{{-- 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.

Categories