Tema
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/viewsy 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>
@endswitchBucles 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>© {{ 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;@sectionlo rellena.@include('partials.menu')inserta otra vista (puedes pasarle datos).@stacky@pushacumulan 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 TarjetaProductoSe 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" />
@endforeachFormularios 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
- Crea un componente anónimo
botonque recibacolor(por defectoazul) y muestre el contenido del slot dentro de un<button>. - Crea una vista
equipo.blade.phpque use el layout y liste un array de personas (nombre y puesto) con@forelse, numerando cada una con$loop->iterationy mostrando "Sin integrantes" si está vacío. - Haz que el menú del layout marque como activo el enlace de la página actual (pista:
request()->routeIs('productos.*')).
Ver solución
- 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>- 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>- 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/viewsy se llaman conview('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,@sectiony@yield. - Los componentes (
x-alerta,x-tarjeta-producto) reutilizan HTML; pasa variables con:atributo. - En formularios:
@csrf,@method('PUT'),old()y@error.