Blade Components Guide
Last Updated: 2026-01-08 Status: Active Audience: Developers
This guide documents Blade component patterns and conventions for the Client Portal application.
Table of Contents
- Overview
- Component Types
- Creating Components
- Props and Slots
- Component Organization
- Common Patterns
- Layouts
Overview
Blade components provide reusable UI elements. Use them to:
- Reduce duplication across views
- Maintain consistent styling
- Encapsulate complex markup
- Create a component library
Component Syntax
{{-- Using components --}}
<x-button>Click me</x-button>
<x-alert type="success" :message="$message" />
<x-card title="Dashboard">
Content here
</x-card>
Component Types
Anonymous Components
Simple components without a class, just a Blade file:
{{-- resources/views/components/alert.blade.php --}}
@props(['type' => 'info', 'message'])
<div class="alert alert-{{ $type }}">
{{ $message }}
</div>
Class-Based Components
Components with PHP logic:
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class Alert extends Component
{
public function __construct(
public string $type = 'info',
public string $message = ''
) {}
public function alertClasses(): string
{
return match ($this->type) {
'success' => 'bg-green-100 text-green-800 border-green-200',
'error' => 'bg-red-100 text-red-800 border-red-200',
'warning' => 'bg-yellow-100 text-yellow-800 border-yellow-200',
default => 'bg-blue-100 text-blue-800 border-blue-200',
};
}
public function render()
{
return view('components.alert');
}
}
{{-- resources/views/components/alert.blade.php --}}
<div {{ $attributes->merge(['class' => 'p-4 rounded border ' . $alertClasses()]) }}>
{{ $message }}
</div>
Creating Components
Generate Component
# Anonymous component (Blade only)
php artisan make:component Alert --view
# Class-based component
php artisan make:component Alert
# Nested component
php artisan make:component Forms/Input
File Locations
app/View/Components/ # Component classes
├── Alert.php
├── Button.php
└── Forms/
└── Input.php
resources/views/components/ # Component views
├── alert.blade.php
├── button.blade.php
└── forms/
└── input.blade.php
Props and Slots
Defining Props
{{-- Anonymous component --}}
@props([
'type' => 'button',
'variant' => 'primary',
'size' => 'md',
'disabled' => false,
])
<button
type="{{ $type }}"
{{ $disabled ? 'disabled' : '' }}
{{ $attributes->merge(['class' => 'btn btn-' . $variant . ' btn-' . $size]) }}
>
{{ $slot }}
</button>
Using Props
{{-- String props --}}
<x-button variant="danger">Delete</x-button>
{{-- Dynamic props (with :) --}}
<x-button :disabled="$isProcessing">Submit</x-button>
{{-- Boolean shorthand --}}
<x-button disabled>Disabled Button</x-button>
Default Slot
{{-- Component --}}
<div class="card">
{{ $slot }}
</div>
{{-- Usage --}}
<x-card>
<p>This goes in the default slot</p>
</x-card>
Named Slots
{{-- Component: card.blade.php --}}
@props(['title' => null])
<div class="card">
@if ($title || isset($header))
<div class="card-header">
{{ $header ?? $title }}
</div>
@endif
<div class="card-body">
{{ $slot }}
</div>
@isset($footer)
<div class="card-footer">
{{ $footer }}
</div>
@endisset
</div>
{{-- Usage --}}
<x-card>
<x-slot:header>
<h3>Custom Header</h3>
</x-slot:header>
<p>Card content here</p>
<x-slot:footer>
<button>Save</button>
</x-slot:footer>
</x-card>
Attribute Bag
{{-- Forward attributes to element --}}
@props(['type' => 'text'])
<input
type="{{ $type }}"
{{ $attributes->merge(['class' => 'form-input']) }}
>
{{-- Usage - class is merged, others passed through --}}
<x-input type="email" name="email" class="w-full" placeholder="Enter email" />
{{-- Renders: --}}
<input type="email" name="email" class="form-input w-full" placeholder="Enter email">
Conditional Classes
<div {{ $attributes->class([
'p-4 rounded',
'bg-green-100' => $type === 'success',
'bg-red-100' => $type === 'error',
'bg-blue-100' => $type === 'info',
]) }}>
{{ $slot }}
</div>
Component Organization
Directory Structure
resources/views/components/
├── layouts/ # Layout components
│ ├── app.blade.php
│ └── guest.blade.php
├── ui/ # Generic UI components
│ ├── alert.blade.php
│ ├── button.blade.php
│ ├── card.blade.php
│ ├── badge.blade.php
│ ├── modal.blade.php
│ └── dropdown.blade.php
├── forms/ # Form components
│ ├── input.blade.php
│ ├── textarea.blade.php
│ ├── select.blade.php
│ ├── checkbox.blade.php
│ └── label.blade.php
├── tables/ # Table components
│ ├── table.blade.php
│ ├── th.blade.php
│ └── td.blade.php
└── domain/ # Domain-specific components
├── invoice-card.blade.php
├── project-status.blade.php
└── client-avatar.blade.php
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Files | kebab-case | invoice-card.blade.php |
| Classes | PascalCase | InvoiceCard.php |
| Usage | x-kebab-case | <x-invoice-card /> |
| Nested | x-folder.name | <x-forms.input /> |
Common Patterns
Form Input Component
{{-- resources/views/components/forms/input.blade.php --}}
@props([
'name',
'label' => null,
'type' => 'text',
'value' => null,
'required' => false,
])
<div>
@if ($label)
<label for="{{ $name }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ $label }}
@if ($required)
<span class="text-red-500">*</span>
@endif
</label>
@endif
<input
type="{{ $type }}"
name="{{ $name }}"
id="{{ $name }}"
value="{{ old($name, $value) }}"
{{ $required ? 'required' : '' }}
{{ $attributes->merge([
'class' => 'block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500'
. ($errors->has($name) ? ' border-red-500' : '')
]) }}
>
@error($name)
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
{{-- Usage --}}
<x-forms.input
name="email"
label="Email Address"
type="email"
:value="$client->email"
required
/>
Button Component
{{-- resources/views/components/ui/button.blade.php --}}
@props([
'type' => 'button',
'variant' => 'primary',
'size' => 'md',
'href' => null,
])
@php
$baseClasses = 'inline-flex items-center justify-center font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors';
$variantClasses = match ($variant) {
'primary' => 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
'secondary' => 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
'danger' => 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
'success' => 'bg-green-600 text-white hover:bg-green-700 focus:ring-green-500',
'outline' => 'border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-blue-500',
default => 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
};
$sizeClasses = match ($size) {
'sm' => 'px-3 py-1.5 text-sm',
'md' => 'px-4 py-2 text-sm',
'lg' => 'px-6 py-3 text-base',
default => 'px-4 py-2 text-sm',
};
$classes = "$baseClasses $variantClasses $sizeClasses";
@endphp
@if ($href)
<a href="{{ $href }}" {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</a>
@else
<button type="{{ $type }}" {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</button>
@endif
{{-- Usage --}}
<x-ui.button type="submit" variant="primary">Save</x-ui.button>
<x-ui.button variant="danger" size="sm">Delete</x-ui.button>
<x-ui.button href="{{ route('clients.index') }}" variant="outline">Cancel</x-ui.button>
Status Badge Component
{{-- resources/views/components/domain/status-badge.blade.php --}}
@props(['status'])
@php
$classes = match ($status->value ?? $status) {
'draft' => 'bg-gray-100 text-gray-800',
'active', 'sent' => 'bg-blue-100 text-blue-800',
'completed', 'paid' => 'bg-green-100 text-green-800',
'overdue' => 'bg-red-100 text-red-800',
'archived' => 'bg-yellow-100 text-yellow-800',
default => 'bg-gray-100 text-gray-800',
};
$label = $status->value ?? $status;
@endphp
<span {{ $attributes->merge(['class' => "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium $classes"]) }}>
{{ ucfirst($label) }}
</span>
{{-- Usage --}}
<x-domain.status-badge :status="$invoice->status" />
<x-domain.status-badge status="active" />
Modal Component
{{-- resources/views/components/ui/modal.blade.php --}}
@props([
'name',
'title' => '',
'maxWidth' => 'md',
])
@php
$maxWidthClass = match ($maxWidth) {
'sm' => 'sm:max-w-sm',
'md' => 'sm:max-w-md',
'lg' => 'sm:max-w-lg',
'xl' => 'sm:max-w-xl',
'2xl' => 'sm:max-w-2xl',
default => 'sm:max-w-md',
};
@endphp
<div
x-data="{ open: false }"
x-on:open-modal.window="if ($event.detail === '{{ $name }}') open = true"
x-on:close-modal.window="if ($event.detail === '{{ $name }}') open = false"
x-on:keydown.escape.window="open = false"
x-show="open"
x-cloak
class="fixed inset-0 z-50 overflow-y-auto"
>
{{-- Backdrop --}}
<div
x-show="open"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 bg-gray-500 bg-opacity-75"
@click="open = false"
></div>
{{-- Modal --}}
<div class="flex min-h-full items-center justify-center p-4">
<div
x-show="open"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-4"
class="relative w-full {{ $maxWidthClass }} bg-white rounded-lg shadow-xl"
>
@if ($title)
<div class="px-6 py-4 border-b">
<h3 class="text-lg font-medium">{{ $title }}</h3>
</div>
@endif
<div class="p-6">
{{ $slot }}
</div>
@isset($footer)
<div class="px-6 py-4 bg-gray-50 border-t flex justify-end gap-3 rounded-b-lg">
{{ $footer }}
</div>
@endisset
</div>
</div>
</div>
{{-- Usage --}}
<x-ui.button @click="$dispatch('open-modal', 'confirm-delete')">Delete</x-ui.button>
<x-ui.modal name="confirm-delete" title="Confirm Deletion" maxWidth="sm">
<p>Are you sure you want to delete this item?</p>
<x-slot:footer>
<x-ui.button variant="outline" @click="$dispatch('close-modal', 'confirm-delete')">
Cancel
</x-ui.button>
<x-ui.button variant="danger" type="submit">
Delete
</x-ui.button>
</x-slot:footer>
</x-ui.modal>
Layouts
App Layout
{{-- resources/views/components/layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased bg-gray-100">
<div class="min-h-screen">
{{-- Navigation --}}
<x-layouts.navigation />
{{-- Page Header --}}
@isset($header)
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
</div>
</header>
@endisset
{{-- Flash Messages --}}
<x-layouts.flash-messages />
{{-- Page Content --}}
<main>
{{ $slot }}
</main>
</div>
</body>
</html>
Using Layout
{{-- resources/views/admin/clients/index.blade.php --}}
<x-layouts.app>
<x-slot:title>Clients - {{ config('app.name') }}</x-slot:title>
<x-slot:header>
<h1 class="text-2xl font-bold">Clients</h1>
</x-slot:header>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
{{-- Page content --}}
</div>
</div>
</x-layouts.app>
Best Practices
Do
- Keep components focused and single-purpose
- Use props for configuration, slots for content
- Provide sensible defaults
- Use
$attributes->merge()for flexibility - Document complex components
- Group related components in directories
Don't
- Put business logic in components
- Create components for one-time use
- Hardcode styles (use props for variants)
- Forget to handle the error state for form inputs
- Nest components too deeply