Desarrollo Web 5-8 minutos

Componentes Blade en Laravel 13: props, slots y plantillas reutilizables

Diego Cortés
Diego Cortés
Full Stack Developer & SEO Specialist
Compartir:
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:

{{-- 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.

Categorías