Laravel Blade components turn repeated markup into reusable, HTML-like tags such as <x-alert> or <x-button>. You write the markup once in resources/views/components, pass data through attributes, and insert content using slots. A component can be a single Blade file (an anonymous component) or a Blade file backed by a PHP class when logic is required.

Open almost any Laravel application that's more than two years old and search for class="rounded-md bg-indigo-600". You'll often find the same button styles copied across 40 templates, each with slight differences. Changing the brand color then becomes a risky find-and-replace task.

This Laravel Blade tutorial explains how to solve that problem with components. You'll learn when to use anonymous components versus class components, how props, slots, and the $attributes bag work, how to build layouts, and how to test your components.

Key Takeaways

  • Start with anonymous components. A single file in resources/views/components with @props is enough for most buttons, cards, alerts, and inputs.

  • Use class components when logic is required, such as database queries, formatting, or dependency injection.

  • Always merge $attributes. This allows callers to add classes, wire:model, or id attributes without editing the component.

  • Use named slots (<x-slot:title>) instead of long prop lists when passing rich HTML into layouts and cards.

  • Components are isolated. Unlike @include, they don't inherit variables from the parent view, making them safer to reuse.

The developers in this article's examples are illustrative composites based on common scenarios.

What Are Laravel Blade Components?

A Blade component is a reusable view with its own inputs. You render it using an x- prefix:

<x-alert type="error" message="Your card was declined." />

Laravel maps the tag to a file by name. For example, <x-alert> loads resources/views/components/alert.blade.php. Nested folders use dots, so <x-forms.input> loads components/forms/input.blade.php.

There are two types of Blade components:

Anonymous Component

Class Component

Files

One Blade file

PHP class plus Blade file

Props declared with

@props directive

Constructor arguments

Logic

Minimal, inline

Full PHP: queries, services, formatting

Created with

make:component Alert --view

make:component Alert

Best for

Buttons, badges, cards, inputs

Data-driven widgets, database-driven dropdowns

Components vs. @include

@include('partials.alert') still works, but an included view inherits all variables from its parent. This is convenient until a partial quietly depends on a $user variable that one page doesn't define.

Components only receive the data you pass to them, along with globally shared data such as $errors. Their inputs are explicit, so you can use them on any page without unexpected dependencies. If you need an isolated partial instead, @includeIsolated gives @include the same behavior.

Anonymous Components: The Fastest Way to Start

Generate a view-only component:

php artisan make:component Alert --view

This creates resources/views/components/alert.blade.php (anonymous component documentation). Declare its props at the top using @props, including any default values:

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

<div {{ $attributes->class([
    'rounded-md p-4 text-sm',
    'bg-blue-50 text-blue-800' => $type === 'info',
    'bg-red-50 text-red-800' => $type === 'error',
]) }} role="alert">
    {{ $message }}
</div>

You can now use it anywhere:

<x-alert message="Profile saved." />
<x-alert type="error" message="Something went wrong." class="mb-6" />

The class="mb-6" attribute isn't a prop, so it goes into the $attributes bag. The class() method merges it with the component's existing classes.

Pass PHP Values With a Colon

Plain attributes are passed as strings. Prefix an attribute with : to pass a PHP expression:

<x-alert :message="$errors->first('email')" />
<x-user-card :user="$user" :show-email="true" />

If the variable name matches the prop name, you can use the shorthand:

<x-user-card :$user />

Attributes use kebab-case in the tag (show-email), while the prop is available as camelCase ($showEmail) inside the component.

Slots: Passing HTML Into Blade Components

Props work well for short strings. For rich content, use slots. Everything between the opening and closing tags becomes $slot:

{{-- components/card.blade.php --}}
<div {{ $attributes->merge(['class' => 'rounded-lg border bg-white p-6 shadow-sm']) }}>
    {{ $slot }}
</div>
<x-card>
    <p>You have <strong>3</strong> unpaid invoices.</p>
</x-card>

Named Slots

A card often needs a header and footer, too. Named slots handle this:

{{-- components/card.blade.php --}}
@props(['header' => null, 'footer' => null])

<div {{ $attributes->merge(['class' => 'rounded-lg border bg-white shadow-sm']) }}>
    @if ($header)
        <div {{ $header->attributes->merge(['class' => 'border-b px-6 py-4 font-semibold']) }}>
            {{ $header }}
        </div>
    @endif

    <div class="p-6">{{ $slot }}</div>

    @if ($footer)
        <div class="border-t px-6 py-3">{{ $footer }}</div>
    @endif
</div>
<x-card>
    <x-slot:header class="text-red-700">Billing</x-slot>

    Your plan renews on 1 December.

    <x-slot:footer>
        <a href="{{ route('billing.show') }}">Manage billing</a>
    </x-slot>
</x-card>

Notice $header->attributes. Slots have their own attributes, allowing callers to style the header without adding new props.

Here's why this matters. Aisha's team had a card partial with nine variables: $title, $titleClass, $subtitle, $footerLink, $footerText, and more. Every new page required another variable. In June, she replaced it with a component using two named slots. The nine variables became zero, and new cards no longer required changes to the shared file.

Want components like these generated for you? StackMivo builds full Laravel apps from a plain-English description, with Blade layouts, forms, and components already wired to your routes and models.

The $attributes Bag: Make Components Flexible

Every attribute that isn't a declared prop goes into $attributes. Rendering it correctly is what makes a reusable component flexible rather than rigid.

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

<button {{ $attributes->merge(['type' => 'button'])->class([
    'inline-flex items-center rounded-md px-4 py-2 text-sm font-medium',
    'bg-indigo-600 text-white hover:bg-indigo-700' => $variant === 'primary',
    'bg-white text-gray-700 border hover:bg-gray-50' => $variant === 'secondary',
]) }}>
    {{ $slot }}
</button>
<x-button type="submit">Save</x-button>
<x-button variant="secondary" wire:click="cancel" class="ml-2">Cancel</x-button>

Here's how merging behaves:

  • class attributes are appended. The caller's ml-2 is added to the component's classes.

  • Other attributes are replaced. The caller's type="submit" overrides the default type="button".

  • Everything else passes through. wire:click, id, aria-label, and data-* reach the <button> unchanged.

Useful helper methods on the bag include:

{{ $attributes->only(['id', 'name']) }}
{{ $attributes->except(['class']) }}
{{ $attributes->whereStartsWith('wire:model') }}
@if ($attributes->has('disabled')) ... @endif
{{ $attributes->get('placeholder', 'Search...') }}

whereStartsWith('wire:model') is useful in form inputs. It allows you to forward Livewire bindings to the <input> and other attributes to a wrapper <div>.

Class-Based Blade Components

When a component needs actual logic, generate a class:

php artisan make:component CategorySelect

This creates app/View/Components/CategorySelect.php and resources/views/components/category-select.blade.php. All public properties and public methods are available in the view:

<?php

namespace App\View\Components;

use App\Models\Category;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Illuminate\View\Component;

class CategorySelect extends Component
{
    public Collection $categories;

    public function __construct(public ?int $selected = null)
    {
        // Runs every time the component renders; cache if the list is large.
        $this->categories = Category::orderBy('name')->get(['id', 'name']);
    }

    public function isSelected(int $id): bool
    {
        return $id === $this->selected;
    }

    public function render(): View|Closure|string
    {
        return view('components.category-select');
    }
}
{{-- components/category-select.blade.php --}}
<select {{ $attributes->merge(['class' => 'rounded-md border-gray-300']) }}>
    @foreach ($categories as $category)
        <option value="{{ $category->id }}" @selected($isSelected($category->id))>
            {{ $category->name }}
        </option>
    @endforeach
</select>
<x-category-select name="category_id" :selected="$post->category_id" />

Each page now gets the same sorted list, with the query kept in one place.

Two more class features are worth knowing:

  • shouldRender(): Return false to skip rendering entirely, such as hiding a promo banner for paying customers.

  • Dependency injection: Type-hint services in the constructor before your data arguments, and Laravel's container resolves them.

Keep queries in class components lightweight. A component rendered 50 times in a loop runs its constructor 50 times. When rendering inside a loop, load the data in the controller and pass it in.

Build Layouts With Blade Components

Components can also replace @extends and @section for page layouts. Both approaches are supported, but components keep everything within one syntax. Create components/layout.blade.php:

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

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ $title }}</title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-50">
    {{ $slot }}
</body>
</html>
{{-- resources/views/dashboard.blade.php --}}
<x-layout title="Dashboard">
    <h1 class="text-2xl font-bold">Welcome back, {{ auth()->user()->name }}</h1>
</x-layout>

The route() helpers inside layouts and slots depend on named routes. Our Laravel routing guide explains how to set them up.

The Livewire starter kit uses this pattern, with layout components wrapping every authenticated page. The React, Vue, and Svelte kits use Inertia layouts instead. If you're choosing a starting point, our Laravel starter kit guide compares the Blade-based options with the React and Vue kits.

Organizing Components in a Real App

A well-organized components folder pays off quickly. Here's a structure that scales:

resources/views/components/
├── layout.blade.php
├── button.blade.php
├── card.blade.php
├── forms/
│   ├── input.blade.php        -> <x-forms.input>
│   ├── label.blade.php        -> <x-forms.label>
│   └── error.blade.php        -> <x-forms.error>
└── accordion/
    ├── accordion.blade.php    -> <x-accordion>
    └── item.blade.php         -> <x-accordion.item>

A file named after its own folder, such as accordion/accordion.blade.php, acts as the root component. This means <x-accordion> and <x-accordion.item> can live together.

Share Parent Data With @aware

Child components don't automatically see their parent's props. The @aware directive reads a value from the parent:

{{-- components/accordion/accordion.blade.php --}}
@props(['variant' => 'default'])

<div {{ $attributes->class(['space-y-1' => $variant === 'compact', 'space-y-4' => $variant !== 'compact']) }}>
    {{ $slot }}
</div>
{{-- components/accordion/item.blade.php --}}
@aware(['variant' => 'default'])
@props(['title'])

<details @class(['py-1 text-sm' => $variant === 'compact', 'py-3' => $variant !== 'compact'])>
    <summary>{{ $title }}</summary>
    {{ $slot }}
</details>

Now, <x-accordion variant="compact"> styles every item without repeating the prop. There's one catch: @aware only sees attributes actually passed to the parent. It can't read the parent's @props defaults, so give @aware its own default, as shown above.

Marco learned this the hard way. His agency built admin panels for six clients, and each project started by copying the views from the previous one. By the fourth client, the "same" form input existed in four versions. In January, he moved the inputs, buttons, and modals into a shared components folder with consistent props. New admin screens went from about two days to under one because the team could assemble pages instead of restyling them.

Blade Components and Security

Blade escapes output in {{ }} using htmlspecialchars, including attribute values passed to components. This protects you from most cross-site scripting (XSS).

The risk appears when you use {!! !!} to print raw HTML. Never render user-supplied content without escaping it, even inside a "trusted" component. If a component must accept HTML, pass it through a slot written by your developers, not a prop populated with user input. The OWASP XSS Prevention Cheat Sheet explains why context-aware escaping matters.

Alpine.js Attributes

Another common issue: Alpine.js attributes such as :class conflict with Blade's colon syntax for PHP expressions. Prefix them with a double colon, and Blade renders them as a normal :class attribute for Alpine:

<x-button ::class="{ 'opacity-50': loading }">Save</x-button>

Testing Laravel Blade Components

Laravel can render a component in a test without a route (testing documentation):

// tests/Feature/AlertComponentTest.php (Pest)
it('renders an error alert', function () {
    $view = $this->blade('<x-alert type="error" message="Card declined" />');

    $view->assertSee('Card declined');
    $view->assertSee('bg-red-50', false); // false = don't escape the needle
});

For class components, render the class directly. CategorySelect queries the database, so the test needs a fresh database and some categories:

// tests/Feature/CategorySelectTest.php (Pest)
use App\Models\Category;
use App\View\Components\CategorySelect;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

it('marks the chosen category as selected', function () {
    $books = Category::factory()->create(['name' => 'Books']);
    Category::factory()->create(['name' => 'Music']);

    $view = $this->component(CategorySelect::class, ['selected' => $books->id]);

    $view->assertSee('<option value="'.$books->id.'" selected>', false);
    $view->assertSeeInOrder(['Books', 'Music']);
});

Rendering assertions like these catch the common regression where someone "simplifies" a shared component and breaks it across 30 pages.

Blade Components vs. Livewire vs. Inertia

Blade components render on the server as part of each request. They don't hold state or respond to clicks on their own. When you need interactivity:

  • Add Alpine.js for small client-side behaviors such as dropdowns, tabs, and toggles.

  • Use Livewire for server-driven interactive components written in PHP and Blade.

  • Use Inertia if your team prefers building the UI with React or Vue.

Livewire components render Blade, so everything in this guide applies to them. Our Livewire vs. Inertia comparison explains how to choose between them.

Building your next Laravel app? Describe it in plain English, and StackMivo generates the models, routes, controllers, and Blade or Livewire views. Start free →

<!-- TODO: embed a relevant YouTube video on Laravel Blade components (prefer StackMivo channel; otherwise an authoritative Laravel educator) -->

Laravel Blade Components Checklist

  • Repeated markup (buttons, inputs, cards, alerts) lives in components instead of copy-pasted HTML

  • Anonymous components are used by default; class components only when logic is needed

  • Every component renders $attributes with merge() or class()

  • Rich content uses named slots instead of long prop lists

  • No {!! !!} with user-supplied data

  • No database queries in components rendered inside loops

  • Shared components have at least one rendering test

  • php artisan view:cache runs during deployment

Frequently Asked Questions

What Are Blade Components in Laravel?

Laravel Blade components are reusable view pieces rendered with tags such as <x-alert>. They accept data through attributes, accept HTML through slots, and can be a single Blade file or a Blade file backed by a PHP class.

What Is an Anonymous Component in Laravel?

An anonymous component is a Blade component without a PHP class. It's a single file in resources/views/components, with props declared using @props. Create one with php artisan make:component Name --view.

What's the Difference Between @include and Blade Components?

An @include partial inherits all variables from the parent view. A component only receives the data you pass to it, making components more predictable and easier to reuse.

How Do I Pass a Variable to a Blade Component?

Prefix the attribute with a colon: <x-alert :message="$message" />. Without the colon, the value is passed as a plain string. If the names match, <x-alert :$message /> is a shortcut.

Can I Use Blade Components With Livewire?

Yes. Livewire views are Blade views, so you can use any Blade component inside them and forward wire:model attributes through $attributes.

Where Are Blade Components Stored?

Blade files live in resources/views/components. Class components also have a PHP class in app/View/Components.

Conclusion

Laravel Blade components bring the same discipline to your front end that you already apply to backend code. Start with anonymous components for buttons, inputs, and cards. Pass rich content through named slots, always merge $attributes, and use class components only when you need actual logic.

Choose the piece of markup you've copied most often in your current app, usually a button or form input, and turn it into a component today. Then explore well-known websites built with Laravel to see how a clean component library can scale.

Generate a Laravel app with ready-made Blade components, free to start →