Componentes Blade en Laravel 13: props, slots y plantillas reutilizables
El mismo botón, la misma tarjeta y el mismo alert copiados en diez vistas son el olor a plantilla más común en las apps Laravel. Los componentes Blade capturan ese markup una sola vez: props para los datos, slots para el contenido y $attributes para la flexibilidad.
El problema: markup repetido en cada vista
El olor a plantilla del HTML copiado
Todos lo hemos hecho: el botón de la home, el de la ficha de producto y el del panel admin son el mismo HTML copiado tres veces con pequeños cambios de clase. La app funciona, pero el día en que el diseño cambia el color del botón tienes que abrir diez vistas y editar la misma línea en cada una. Ese es el olor a plantilla clásico: markup duplicado que rompe la consistencia y convierte cada cambio de estilo en una caza de errores.
Qué es un componente Blade y qué resuelve (DRY y consistencia)
Un componente Blade es un fragmento de interfaz reutilizable que se define una sola vez y se invoca como una etiqueta propia: <x-button>, <x-card> o <x-post-card>. Resuelve exactamente el problema anterior: aplicas el principio DRY al markup, garantizas que el botón se vea igual en todas partes y, cuando cambia el diseño, tocas un único archivo. En Laravel 13 (lanzado el 17 de marzo de 2026, con un camino de actualización mínimo desde Laravel 12) conviven dos vías: los componentes anónimos, una simple vista en resources/views/components ideal para UI pura, y los componentes de clase, una clase PHP con lógica, constructor y método render().
Componentes anónimos: la vía rápida
Crear x-button en resources/views/components
Los componentes anónimos son la forma más rápida de empezar: cualquier vista dentro de resources/views/components se convierte automáticamente en un componente con el prefijo x-. Crea el archivo button.blade.php y tendrás un <x-button> listo para usar en toda la app:
Lee también
{{-- resources/views/components/button.blade.php --}}
@props(['variant' => 'primary'])
<button {{ $attributes->merge(['class' => 'btn btn-' . $variant]) }}>
{{ $slot }}
</button>Props con @props y valores por defecto
Las props son los datos que el componente recibe desde la vista que lo usa. La directiva @props las declara y, de paso, les asigna valores por defecto: aquí variant vale primary si no se pasa nada. Así el mismo componente sirve para todas las variantes:
<x-button>Guardar cambios</x-button>
<x-button variant="danger">Eliminar</x-button>Subdirectorios y convención de nombres (x-ui.button)
Cuando la carpeta de componentes crece, los subdirectorios organizan el código: un componente en resources/views/components/ui/button.blade.php se invoca como <x-ui.button>, con punto separando los niveles. La convención es usar kebab-case en los nombres de archivo y mantener una jerarquía coherente para que cualquier desarrollador encuentre un componente sin pensar.
Props, $attributes y la fusión de atributos
Props declaradas vs atributos sueltos: cómo viajan los datos
La regla de oro: todo atributo que escribas en la etiqueta y no esté declarado como prop aterriza en la bolsa $attributes. Si llamas a <x-button id="btn-save" data-confirm="true">, id y data-confirm viajan en $attributes y se renderizan en el elemento donde los imprimas. Eso permite pasar cualquier atributo HTML, evento o clase sin tocar el componente.
$attributes->merge() y el merge especial de la clase
La fusión de atributos es lo que da flexibilidad real. {{ $attributes->merge(['class' => 'btn btn-' . $variant]) }} combina los atributos que llegan desde fuera con los valores por defecto del componente. El detalle que confunde a casi todos: en el caso especial de class, el merge concatena en lugar de sobrescribir. Si el llamador pasa class="w-full" y el componente aporta btn btn-primary, el resultado es btn btn-primary w-full, no una clase reemplazando a la otra. Con el resto de atributos, el valor del llamador gana.
$attributes->class() para listas condicionales de clases
Para clases condicionales existe $attributes->class([...]), disponible desde Laravel 9: recibe un array donde cada clase se incluye si su valor es verdadero. Es la forma limpia de decir "añade la clase active solo cuando corresponda" sin construir cadenas a mano:
<div {{ $attributes->class(['card', 'card-active' => $active]) }}>
{{ $slot }}
</div>Slots: contenido flexible dentro del componente
El slot por defecto {{ $slot }}
Si las props son los datos, los slots son el contenido: el HTML que se escribe entre la apertura y el cierre de la etiqueta. El componente lo imprime con {{ $slot }}, y así el mismo <x-card> sirve para una noticia, un producto o un mensaje de bienvenida.
Named slots con x-slot:title y sus atributos
Cuando un componente necesita varias zonas de contenido, los named slots las separan. En la llamada se definen con <x-slot:title> (o <x-slot name="title">) y en la vista del componente se imprimen como {{ $title }}. Un alert con título y cuerpo:
{{-- resources/views/components/alert.blade.php --}}
@props(['type' => 'info'])
<div class="alert alert-{{ $type }}" {{ $attributes }}>
<h3>{{ $title }}</h3>
{{ $slot }}
</div><x-alert type="success">
<x-slot:title>Publicado</x-slot:title>
El artículo ya está visible en el blog.
</x-alert>hasActualContent() para detectar contenido real
Desde Laravel 11, $slot->hasActualContent() devuelve si el slot contiene contenido real o solo HTML vacío. Es útil para renderizar condicionalmente wrappers: por ejemplo, no pintar un contenedor de acciones si el slot de acciones está vacío.
Componentes de clase: lógica dentro del componente
php artisan make:component y el patrón clase + vista
Cuando el componente necesita lógica PHP — consultar datos, calcular clases, decidir qué mostrar — pasas a los componentes de clase. Se generan con php artisan make:component Badge, que crea la clase en app/View/Components/Badge.php y la vista resources/views/components/badge.blade.php.
Props por propiedades públicas y constructor
Las props se declaran como propiedades públicas y se reciben por el constructor, con valores por defecto para los opcionales:
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class Badge extends Component
{
public function __construct(
public string $status,
public bool $withDot = false,
) {}
public function render()
{
return view('components.badge');
}
}render() con vistas inline y acceso a $component
El método render() devuelve la vista, pero también puede devolver una cadena con una vista inline. Dentro de render() tienes acceso al propio componente a través de $component->name (el nombre del componente), $component->attributes (los atributos recibidos) y $component->slot (el contenido del slot), algo que la documentación oficial de 13.x documenta y que permite decidir la vista según los datos de entrada.
public function render()
{
if ($this->status === 'published') {
return '<span {{ $attributes }} class="badge badge-green">{{ $slot }}</span>';
}
return view('components.badge');
}El layout como componente y los componentes dinámicos
x-app-layout y los slots de layout (el patrón de los starter kits)
El patrón recomendado para proyectos nuevos es modelar el layout como un componente con slots, el enfoque que usan los propios starter kits (Breeze y Jetstream) frente a la herencia clásica con @extends y @section:
<x-app-layout>
<x-slot name="header">
<h1>Panel de administración</h1>
</x-slot>
<div class="py-12">
<x-post-card :post="$post" />
</div>
</x-app-layout>La vista del layout define {{ $header }} y {{ $slot }} en sus posiciones, y las páginas solo se preocupan del contenido, no de repetir la estructura del documento.
x-dynamic-component para renderizar componentes configurables
Los componentes dinámicos se renderizan en tiempo de ejecución: <x-dynamic-component :component="$blockType" /> pinta el componente cuyo nombre está en la variable. Es la pieza perfecta para UI configurable por datos — listas de bloques guardadas en base de datos o en config — donde no sabes de antemano qué componente tocará.
Ejemplo real: componentes en el blog de un Laravel 13
post-card reutilizado en la home y en artículos relacionados
Un blog construido con Laravel 13 es el caso perfecto. El componente x-post-card recibe el Post por prop — usando el query scope published de Eloquent — y se reutiliza en la portada, en la lista de artículos relacionados y en cualquier sección que liste entradas. Cambias el diseño de la tarjeta una vez y toda la web se actualiza.
alert con variantes por prop y un badge de clase con lógica
En el panel admin, un x-alert con variantes (info, success, danger) comunica el resultado de las acciones, y el x-badge de clase muestra el estado de cada post (published o draft) decidiendo el color desde la lógica PHP en lugar de desde la vista.
La vista antes y después
El antes: veinte líneas de markup repetido en cada vista, con clases inline y condiciones copiadas. El después: tres líneas con componentes que reciben solo los datos que cambian. El mantenimiento pasa de "editar en diez sitios" a "editar un componente".
Testing de componentes
renderComponent()->assertSee() y probar props, slots y clases
Los componentes se testean como el resto de vistas con Pest o PHPUnit. renderComponent() pinta el componente y sus aserciones comprueban props, slots, atributos y clases generadas:
it('renders the button with the right variant', function () {
$this->renderComponent(Button::class, ['variant' => 'danger'])
->assertSee('btn-danger')
->assertSee('Eliminar');
});Así, un cambio de diseño que rompe una clase se detecta en la suite antes de llegar a producción.
Conclusión
Los componentes Blade convierten el markup duplicado en una librería de piezas: anónimos para UI pura, de clase cuando hay lógica, $attributes para la flexibilidad, slots para el contenido y el layout como componente para la estructura. Empieza por el componente que más repitas en tu app — seguramente un botón o una tarjeta — y deja que el resto del sistema crezca a partir de ahí. Si quieres seguir construyendo sobre estas bases, en el blog tienes guías de query scopes de Eloquent, de assets con Vite y de interactividad con HTMX para completar el flujo de una app Laravel 13.


