Skip to content

Blade: plantillas y componentes ​

Blade es el motor de plantillas de Laravel. Te permite escribir HTML con datos dinámicos de forma limpia y segura, reutilizar partes (encabezado, menú, pie) con layouts y crear componentes como <x-alerta> o <x-boton>. En esta lección construirás la base visual de cualquier proyecto.

Concepto ​

Si en PHP puro hiciste plantillas con include, ya sabes lo tedioso que es mezclar <?php echo ... ?> con HTML. Blade es como un "PHP con superpoderes" para vistas:

  • Las vistas viven en resources/views y terminan en .blade.php.
  • Se compilan a PHP normal y se guardan en caché: son igual de rápidas que PHP puro.
  • Las dobles llaves imprimen datos escapando el HTML automáticamente, lo que te protege de ataques XSS.
  • Las directivas que empiezan con @ (@if, @foreach, @csrf...) reemplazan las estructuras de control de PHP.

La vista resources/views/productos/index.blade.php se llama desde el controlador con view('productos.index'): los puntos representan carpetas.

Mostrar datos ​

blade
{{-- Esto es un comentario de Blade: no aparece en el HTML final --}}

{{-- Imprimir una variable (escapada: seguro contra XSS) --}}
<h1>Hola, {{ $nombre }}</h1>

{{-- Cualquier expresión PHP --}}
<p>Total: ${{ number_format($total, 2) }}</p>
<p>Año: {{ date('Y') }}</p>

{{-- Valor por defecto si la variable es null --}}
<p>{{ $apodo ?? 'Sin apodo' }}</p>

{{-- Imprimir SIN escapar: solo con HTML 100% confiable (nunca datos del usuario) --}}
<div>{!! $contenidoHtmlDelAdmin !!}</div>

{{-- Mostrar dobles llaves literales (útil si usas Vue o Alpine): anteponer @ --}}
<p>@{{ esto no lo procesa Blade }}</p>

Cuidado con la versión sin escapar

La sintaxis con llave y dos signos de admiración imprime HTML tal cual. Si la usas con algo que escribió un usuario (un comentario, un nombre), cualquiera puede inyectar JavaScript en tu sitio. Úsala solo con contenido generado por ti o limpiado con una librería como HTML Purifier.

Estructuras de control ​

blade
{{-- Condicionales --}}
@if ($stock > 10)
    <span class="verde">Disponible</span>
@elseif ($stock > 0)
    <span class="naranja">¡Últimas {{ $stock }} piezas!</span>
@else
    <span class="rojo">Agotado</span>
@endif

{{-- unless = "si NO" --}}
@unless ($usuario->verificado)
    <p>Verifica tu correo para continuar.</p>
@endunless

{{-- isset / empty --}}
@isset($cupon)
    <p>Cupón aplicado: {{ $cupon }}</p>
@endisset

{{-- Usuario autenticado o invitado --}}
@auth
    <a href="{{ route('dashboard') }}">Mi panel</a>
@endauth
@guest
    <a href="{{ route('login') }}">Iniciar sesión</a>
@endguest

{{-- Switch --}}
@switch($pedido->estado)
    @case('pendiente')
        <span>⏳ Pendiente</span>
        @break
    @case('enviado')
        <span>🚚 Enviado</span>
        @break
    @default
        <span>✅ Entregado</span>
@endswitch

Bucles y la variable $loop ​

blade
<ul>
    {{-- forelse: como foreach, pero con bloque para lista vacía --}}
    @forelse ($productos as $producto)
        <li class="{{ $loop->even ? 'fila-par' : '' }}">
            {{ $loop->iteration }}. {{ $producto->nombre }}

            @if ($loop->first)
                <strong>(¡El más nuevo!)</strong>
            @endif
        </li>
    @empty
        <li>No hay productos todavía.</li>
    @endforelse
</ul>

@for ($i = 1; $i <= 5; $i++)
    ⭐
@endfor

$loop tiene: index (desde 0), iteration (desde 1), count, first, last, even, odd, remaining y parent (en bucles anidados).

Clases y atributos condicionales ​

blade
{{-- @class agrega clases solo si la condición es verdadera --}}
<span @class([
    'insignia',
    'insignia-verde' => $producto->activo,
    'insignia-gris'  => ! $producto->activo,
])>
    {{ $producto->activo ? 'Activo' : 'Inactivo' }}
</span>

<input type="checkbox" name="activo" @checked($producto->activo)>

<select name="categoria_id">
    @foreach ($categorias as $categoria)
        <option value="{{ $categoria->id }}" @selected($categoria->id == $producto->categoria_id)>
            {{ $categoria->nombre }}
        </option>
    @endforeach
</select>

<button @disabled($carrito->vacio())>Pagar</button>

Layouts con componentes (forma moderna) ​

Un layout es el "molde" de todas tus páginas: <head>, menú y pie. La forma recomendada en Laravel 11/12 es usar un componente de layout.

1. Crea el layout ​

Los componentes anónimos viven en resources/views/components. Crea resources/views/components/layout.blade.php:

blade
{{-- resources/views/components/layout.blade.php --}}
@props(['titulo' => 'Mi Negocio'])

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ $titulo }} | {{ config('app.name') }}</title>

    {{-- Carga CSS y JS compilados por Vite --}}
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
    <header>
        <nav>
            <a href="{{ route('inicio') }}">Inicio</a>
            <a href="{{ route('productos.index') }}">Productos</a>
            <a href="{{ route('contacto') }}">Contacto</a>
        </nav>
    </header>

    <main>
        {{-- Mensaje flash de éxito enviado con ->with('exito', '...') --}}
        @if (session('exito'))
            <div class="alerta-exito">{{ session('exito') }}</div>
        @endif

        {{-- Aquí se inserta el contenido de cada página --}}
        {{ $slot }}
    </main>

    <footer>
        <p>&copy; {{ date('Y') }} {{ config('app.name') }}</p>
    </footer>
</body>
</html>

2. Úsalo en una página ​

blade
{{-- resources/views/productos/index.blade.php --}}
<x-layout titulo="Productos">
    <h1>Nuestros productos</h1>

    @foreach ($productos as $producto)
        <article>
            <h2>{{ $producto->nombre }}</h2>
            <p>${{ number_format($producto->precio, 2) }}</p>
        </article>
    @endforeach

    {{-- Enlaces de paginación (si usaste ->paginate()) --}}
    {{ $productos->links() }}
</x-layout>

Todo lo que pongas entre la etiqueta de apertura y cierre del componente llega al layout como la variable $slot.

Slots con nombre ​

Para inyectar contenido en más de un lugar del layout:

blade
{{-- En el layout: --}}
<head>
    <title>{{ $titulo ?? 'Mi Negocio' }}</title>
    {{ $estilos ?? '' }}
</head>

{{-- En la página: --}}
<x-layout>
    <x-slot:titulo>Contacto</x-slot:titulo>
    <x-slot:estilos>
        <link rel="stylesheet" href="/css/mapa.css">
    </x-slot:estilos>

    <h1>Escríbenos</h1>
</x-layout>

Layouts con herencia (forma clásica) ​

Verás mucho este estilo en proyectos existentes, así que conviene conocerlo:

blade
{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="es">
<head>
    <title>@yield('titulo', 'Mi Negocio')</title>
    @stack('estilos')
</head>
<body>
    @include('partials.menu')

    <main>
        @yield('contenido')
    </main>

    @stack('scripts')
</body>
</html>
blade
{{-- resources/views/inicio.blade.php --}}
@extends('layouts.app')

@section('titulo', 'Inicio')

@section('contenido')
    <h1>Bienvenido</h1>
    @include('partials.tarjeta', ['texto' => 'Envíos a todo México'])
@endsection

@push('scripts')
    <script src="/js/carrusel.js"></script>
@endpush
  • @yield('nombre') marca un hueco; @section lo rellena.
  • @include('partials.menu') inserta otra vista (puedes pasarle datos).
  • @stack y @push acumulan scripts o estilos desde varias vistas.

Componentes reutilizables ​

Componente anónimo (solo vista) ​

blade
{{-- resources/views/components/alerta.blade.php --}}
@props([
    'tipo' => 'info',   // valor por defecto
])

@php
    $colores = [
        'info'    => 'background:#e0f2fe;color:#075985;',
        'exito'   => 'background:#dcfce7;color:#166534;',
        'error'   => 'background:#fee2e2;color:#991b1b;',
    ];
@endphp

{{-- $attributes permite añadir clases u otros atributos desde fuera --}}
<div {{ $attributes->merge(['class' => 'alerta']) }} style="{{ $colores[$tipo] ?? $colores['info'] }} padding:12px; border-radius:8px;">
    {{ $slot }}
</div>

Uso:

blade
<x-alerta tipo="exito">¡Pedido confirmado!</x-alerta>
<x-alerta tipo="error" class="mt-4" id="error-pago">El pago fue rechazado.</x-alerta>

{{-- Para pasar una variable PHP, antepón dos puntos al atributo --}}
<x-alerta :tipo="$hayError ? 'error' : 'exito'">{{ $mensaje }}</x-alerta>

Componente con clase ​

Cuando el componente necesita lógica, créalo con clase:

powershell
php artisan make:component TarjetaProducto

Se crean app/View/Components/TarjetaProducto.php y resources/views/components/tarjeta-producto.blade.php:

php
<?php
// app/View/Components/TarjetaProducto.php

namespace App\View\Components;

use App\Models\Producto;
use Illuminate\View\Component;
use Illuminate\View\View;

class TarjetaProducto extends Component
{
    // Las propiedades públicas están disponibles en la vista
    public function __construct(public Producto $producto)
    {
    }

    // Método disponible en la vista como $precioFormateado()
    public function precioFormateado(): string
    {
        return '$' . number_format($this->producto->precio, 2) . ' MXN';
    }

    public function render(): View
    {
        return view('components.tarjeta-producto');
    }
}
blade
{{-- resources/views/components/tarjeta-producto.blade.php --}}
<article class="tarjeta">
    <h3>{{ $producto->nombre }}</h3>
    <p>{{ $precioFormateado() }}</p>
    <a href="{{ route('productos.show', $producto) }}">Ver detalle</a>
</article>
blade
{{-- Uso --}}
@foreach ($productos as $producto)
    <x-tarjeta-producto :producto="$producto" />
@endforeach

Formularios en Blade ​

blade
<x-layout titulo="Editar producto">
    <h1>Editar {{ $producto->nombre }}</h1>

    {{-- Lista de errores de validación --}}
    @if ($errors->any())
        <x-alerta tipo="error">
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </x-alerta>
    @endif

    <form action="{{ route('productos.update', $producto) }}" method="POST">
        @csrf               {{-- token anti-CSRF: obligatorio en POST --}}
        @method('PUT')      {{-- simula PUT con un campo oculto _method --}}

        <label for="nombre">Nombre</label>
        {{-- old() recupera lo escrito si la validación falló --}}
        <input id="nombre" name="nombre" value="{{ old('nombre', $producto->nombre) }}">
        @error('nombre')
            <small class="error">{{ $message }}</small>
        @enderror

        <label for="precio">Precio</label>
        <input id="precio" name="precio" type="number" step="0.01"
               value="{{ old('precio', $producto->precio) }}">
        @error('precio')
            <small class="error">{{ $message }}</small>
        @enderror

        <button type="submit">Guardar cambios</button>
    </form>

    {{-- Botón de eliminar: otro formulario con DELETE --}}
    <form action="{{ route('productos.destroy', $producto) }}" method="POST"
          onsubmit="return confirm('¿Seguro que quieres eliminarlo?')">
        @csrf
        @method('DELETE')
        <button type="submit">Eliminar</button>
    </form>
</x-layout>

Buena práctica

Organiza tus vistas por recurso: resources/views/productos/index.blade.php, create, edit, show. Los fragmentos repetidos (formulario compartido entre crear y editar) van en un componente o en un parcial como productos/_form.blade.php.

Error común

Escribir lógica pesada en la vista (consultas a la base de datos dentro de un @foreach). La vista solo debe mostrar; las consultas se preparan en el controlador. Además de ser más ordenado, evita el problema N+1 que verás en relaciones.

Error común

El componente no se encuentra ("Unable to locate a class or view for component"). Revisa que el archivo esté en resources/views/components/ y que el nombre use kebab-case: tarjeta-producto.blade.php se usa con la etiqueta x-tarjeta-producto.

Ejercicios ​

  1. Crea un componente anónimo boton que reciba color (por defecto azul) y muestre el contenido del slot dentro de un <button>.
  2. Crea una vista equipo.blade.php que use el layout y liste un array de personas (nombre y puesto) con @forelse, numerando cada una con $loop->iteration y mostrando "Sin integrantes" si está vacío.
  3. Haz que el menú del layout marque como activo el enlace de la página actual (pista: request()->routeIs('productos.*')).
Ver solución
  1. Componente botón:
blade
{{-- resources/views/components/boton.blade.php --}}
@props(['color' => 'azul'])

@php
    $fondos = ['azul' => '#2563eb', 'verde' => '#16a34a', 'rojo' => '#dc2626'];
@endphp

<button {{ $attributes->merge(['type' => 'button']) }}
        style="background:{{ $fondos[$color] ?? $fondos['azul'] }};color:#fff;padding:8px 16px;border:0;border-radius:6px;">
    {{ $slot }}
</button>
blade
<x-boton>Aceptar</x-boton>
<x-boton color="rojo" type="submit">Eliminar</x-boton>
  1. Vista del equipo. Ruta:
php
Route::get('/equipo', function () {
    $personas = [
        ['nombre' => 'Ana López', 'puesto' => 'Diseñadora'],
        ['nombre' => 'Luis Pérez', 'puesto' => 'Desarrollador'],
    ];
    return view('equipo', compact('personas'));
})->name('equipo');
blade
{{-- resources/views/equipo.blade.php --}}
<x-layout titulo="Equipo">
    <h1>Nuestro equipo</h1>
    <ul>
        @forelse ($personas as $persona)
            <li>{{ $loop->iteration }}. {{ $persona['nombre'] }} — {{ $persona['puesto'] }}</li>
        @empty
            <li>Sin integrantes</li>
        @endforelse
    </ul>
</x-layout>
  1. Menú con enlace activo:
blade
<nav>
    <a href="{{ route('inicio') }}" @class(['activo' => request()->routeIs('inicio')])>Inicio</a>
    <a href="{{ route('productos.index') }}" @class(['activo' => request()->routeIs('productos.*')])>Productos</a>
    <a href="{{ route('contacto') }}" @class(['activo' => request()->routeIs('contacto')])>Contacto</a>
</nav>

Resumen ​

  • Las vistas Blade están en resources/views y se llaman con view('carpeta.archivo').
  • Las dobles llaves imprimen datos escapados (seguro); la versión con admiraciones no escapa (peligroso con datos de usuarios).
  • Directivas: @if, @foreach, @forelse, @auth, @guest, @class, @checked, @selected.
  • Usa un componente de layout (resources/views/components/layout.blade.php) con $slot; también existe la herencia clásica con @extends, @section y @yield.
  • Los componentes (x-alerta, x-tarjeta-producto) reutilizan HTML; pasa variables con :atributo.
  • En formularios: @csrf, @method('PUT'), old() y @error.

Academia Web — aprende, copia y construye.