Skip to main content
Back to ScopeForged

ScopeForged Documentation

Technical documentation, guides, and feature references for the ScopeForged client portal.

Frontend & UI/Blade Components

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

  1. Overview
  2. Component Types
  3. Creating Components
  4. Props and Slots
  5. Component Organization
  6. Common Patterns
  7. 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

TypeConventionExample
Fileskebab-caseinvoice-card.blade.php
ClassesPascalCaseInvoiceCard.php
Usagex-kebab-case<x-invoice-card />
Nestedx-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" />
{{-- 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