php elephant sticker
Photo by RealToughCandy.com on Pexels.com
目次

[Guía práctica] Construir un sistema de diseño con componentes Blade de Laravel — Diseño de componentes UI que equilibra reutilización, mantenibilidad y accesibilidad

Lo que aprenderás en este artículo (puntos clave)

  • Cómo elegir entre componentes Blade (basados en clase / anónimos) y plantillas parciales (@include)
  • Cómo modularizar elementos de “UI frágil” como botones, enlaces, formularios, modales y notificaciones para mantenerlos robustos
  • Cómo diseñar APIs (props / slots) que no se rompan incluso cuando aumentan las variantes (color, tamaño, estado)
  • Consejos para hacer que la accesibilidad sea el valor por defecto: gestión del foco, visualización de errores, aria-*, señales no dependientes del color
  • Estructuras de directorios y prácticas operativas que minimizan cambios cuando se actualizan diseños o branding
  • Perspectivas de testing (Feature / Dusk) para prevenir regresiones en componentes UI

Público objetivo (¿a quién beneficia?)

  • Ingenieros Laravel de nivel principiante a intermedio: quienes desean estabilizar implementaciones UI cada vez más dolorosas mediante la componentización a medida que crecen las pantallas.
  • Líderes técnicos / diseñadores: quienes buscan formalizar las “reglas del lado de implementación” de un sistema de diseño.
  • Especialistas en QA / accesibilidad: quienes quieren estandarizar la usabilidad de formularios y modales y reducir regresiones.
  • Equipos de CS / operaciones: quienes buscan reducir inconsistencias en textos, etiquetas y mensajes de error—y con ello reducir consultas.

Nivel de accesibilidad: ★★★★★

Todos los componentes se diseñan bajo el supuesto de que son “totalmente operables con teclado”, “no dependen solo del color” y “son comprensibles mediante lectores de pantalla”. Ejemplos concretos incluyen label, aria-describedby, aria-invalid, regiones vivas y gestión del foco.


1. Introducción: la componentización de UI no es solo coherencia visual

A medida que crece el número de pantallas en una aplicación Laravel, aparecen repetidamente botones, formularios y alertas similares. Al principio, copiar y pegar funciona, pero en cuanto se solicita un cambio de diseño o de texto, aparece la pesadilla de corregir cada página. Además, las consideraciones de accesibilidad (etiquetas, asociaciones de error, manejo del foco, independencia del color) son fáciles de pasar por alto cuando se gestionan página por página.

Al tratar los componentes Blade como la “implementación de un sistema de diseño”, puedes encapsular la corrección de la UI dentro de los componentes. Si los componentes son correctos, la corrección escala a medida que crece el número de pantallas. Ese es el objetivo.


2. Conceptos básicos de componentes Blade: cuándo usar componentes de clase, componentes anónimos o includes

2.1 Conclusión rápida (si dudas, usa esto)

  • Componentes anónimos (resources/views/components/*.blade.php): componentes principalmente visuales, completados mediante props.
  • Componentes basados en clase (app/View/Components): cuando se requiere lógica o formateo (generación de etiquetas, generación de IDs, comprobaciones de permisos, transformación de datos).
  • Includes (@include): parciales temporales cuando la API aún no está definida.

Para empezar en pequeño, los componentes anónimos son los más sencillos. Cuando la lógica crece, promoverlos a componentes de clase es un paso natural.


3. Definir el “conjunto mínimo” de un sistema de diseño

Los componentes que aportan más valor cuando se estandarizan primero son:

  1. Botones (incluidos enlaces)
  2. Entradas de formulario (texto, select, checkbox)
  3. Visualización de errores (resumen + nivel de campo)
  4. Notificaciones (éxito / advertencia / fallo)
  5. Modales (diálogos)

Aquí es donde con más frecuencia surgen problemas de accesibilidad y de impacto por cambios de diseño. Estandarizar solo estos puede estabilizar drásticamente la experiencia de usuario.


4. Ejemplo: componentes de botón (separar botones y enlaces por responsabilidad)

Botones y enlaces pueden parecer similares, pero cumplen funciones distintas:

  • Botones: acciones (enviar, guardar, eliminar)
  • Enlaces: navegación (cambio de página)

Cuanto más quieras unificar su apariencia, más importante es separarlos como componentes.

4.1 x-button (botón)

resources/views/components/button.blade.php

@props([
  'variant' => 'primary',  // primary|secondary|danger
  'size' => 'md',          // sm|md|lg
  'type' => 'button',
  'disabled' => false,
])

@php
  $base = 'inline-flex items-center justify-center rounded border font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2';
  $sizes = [
    'sm' => 'text-sm px-3 py-1.5',
    'md' => 'text-base px-4 py-2',
    'lg' => 'text-lg px-5 py-3',
  ][$size] ?? 'text-base px-4 py-2';

  $variants = [
    'primary' => 'bg-blue-600 text-white border-blue-600 hover:bg-blue-700 focus-visible:ring-blue-600',
    'secondary' => 'bg-white text-gray-900 border-gray-300 hover:bg-gray-50 focus-visible:ring-gray-400',
    'danger' => 'bg-red-600 text-white border-red-600 hover:bg-red-700 focus-visible:ring-red-600',
  ][$variant] ?? 'bg-blue-600 text-white border-blue-600 hover:bg-blue-700 focus-visible:ring-blue-600';

  $disabledClass = $disabled ? 'opacity-60 cursor-not-allowed' : '';
@endphp

<button type="{{ $type }}"
  {{ $attributes->merge(['class' => "$base $sizes $variants $disabledClass"]) }}
  @disabled($disabled)
  aria-disabled="{{ $disabled ? 'true' : 'false' }}"
>
  {{ $slot }}
</button>

Puntos clave

  • Los anillos de foco (focus-visible:ring) son estándar.
  • Los estados deshabilitados incluyen señales visuales y disabled / aria-disabled.
  • “Danger” se apoya en el significado del texto (p. ej., “Eliminar”), no solo en el color.

4.2 x-link-button (ancla)

resources/views/components/link-button.blade.php

@props([
  'href',
  'variant' => 'primary',
  'size' => 'md',
])

<a href="{{ $href }}"
  {{ $attributes->merge(['class' => 'inline-flex items-center justify-center rounded border font-medium underline-offset-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2']) }}
>
  {{ $slot }}
</a>

Aunque se estilice como botón, mantener los enlaces como <a> preserva la accesibilidad correcta y el comportamiento esperado.


5. Ejemplo: componentes de entrada de formulario (el trío etiqueta–ayuda–error)

Los formularios se vuelven mucho más fáciles cuando “etiqueta”, “texto de ayuda” y “error” se proporcionan como un conjunto.

5.1 x-field (envoltorio)

resources/views/components/field.blade.php

@props([
  'id',
  'label',
  'help' => null,
  'required' => false,
  'error' => null,
])

<div {{ $attributes->merge(['class' => 'mb-4']) }}>
  <label for="{{ $id }}" class="block font-medium">
    {{ $label }}
    @if($required)
      <span aria-hidden="true">(Obligatorio)</span>
      <span class="sr-only">Obligatorio</span>
    @endif
  </label>

  @if($help)
    <p id="{{ $id }}-help" class="text-sm text-gray-600">{{ $help }}</p>
  @endif

  <div class="mt-1">
    {{ $slot }}
  </div>

  @if($error)
    <p id="{{ $id }}-error" class="text-sm text-red-700">{{ $error }}</p>
  @endif
</div>

5.2 x-input

resources/views/components/input.blade.php

@props([
  'id',
  'name',
  'type' => 'text',
  'value' => null,
  'error' => null,
  'helpId' => null,
])

@php
  $desc = [];
  if ($helpId) $desc[] = $helpId;
  if ($error) $desc[] = "{$id}-error";
  $describedBy = count($desc) ? implode(' ', $desc) : null;
@endphp

<input
  id="{{ $id }}"
  name="{{ $name }}"
  type="{{ $type }}"
  value="{{ old($name, $value) }}"
  aria-invalid="{{ $error ? 'true' : 'false' }}"
  @if($describedBy) aria-describedby="{{ $describedBy }}" @endif
  {{ $attributes->merge(['class' => 'w-full border rounded px-3 py-2']) }}
>

5.3 Ejemplo de uso (lado de la página)

@php $emailError = $errors->first('email'); @endphp

<x-field id="email" label="Dirección de correo" :required="true"
  help="Ejemplo: hanako@example.com"
  :error="$emailError"
>
  <x-input id="email" name="email" type="email" :error="$emailError" helpId="email-help"
    autocomplete="email" />
</x-field>

Con esta estructura, las páginas solo pasan etiquetas y atributos; las asociaciones de error y el soporte para lectores de pantalla se gestionan automáticamente.


6. Estandarizar resúmenes de error (reducir usuarios “perdidos”)

resources/views/components/error-summary.blade.php

@props(['errors'])

@if($errors->any())
  <div id="error-summary" role="alert" tabindex="-1" class="border p-3 mb-4">
    <h2 class="font-semibold">Por favor, revisa los errores a continuación.</h2>
    <ul class="list-disc pl-5">
      @foreach($errors->all() as $msg)
        <li>{{ $msg }}</li>
      @endforeach
    </ul>
  </div>

  <script>
    (function(){
      const el = document.getElementById('error-summary');
      if (el) el.focus();
    })();
  </script>
@endif

Puntos clave

  • Enfoca automáticamente cuando se muestra para que los lectores de pantalla anuncien el estado.
  • Como componente, el comportamiento se mantiene consistente en todos los formularios.

7. Estandarizar notificaciones (éxito / advertencia / error)

resources/views/components/notice.blade.php

@props([
  'type' => 'info', // info|success|warning|danger
])

@php
  $styles = [
    'info' => 'border-blue-200 bg-blue-50 text-blue-900',
    'success' => 'border-green-200 bg-green-50 text-green-900',
    'warning' => 'border-yellow-200 bg-yellow-50 text-yellow-900',
    'danger' => 'border-red-200 bg-red-50 text-red-900',
  ][$type] ?? 'border-blue-200 bg-blue-50 text-blue-900';
@endphp

<div role="status" aria-live="polite" class="border rounded p-3 mb-4 {{ $styles }}">
  {{ $slot }}
</div>

Puntos clave

  • Reserva role="alert" solo para errores críticos; usa status para actualizaciones normales.
  • Acompaña siempre el color con texto claro (p. ej., “Guardado correctamente”).

8. Modales (diálogos): un área difícil, así que protégela con componentes

Los modales están llenos de trampas de accesibilidad. Como mínimo:

  • Mover el foco al modal al abrirse
  • Atraparlo dentro del modal
  • Devolver el foco al disparador al cerrarse
  • Permitir cerrar con Esc y evitar el scroll del fondo

Lograrlo perfectamente solo con Blade es difícil, por lo que un JS mínimo (Alpine.js, Stimulus) es práctico. A continuación, un ejemplo conceptual.

resources/views/components/modal.blade.php

@props(['id', 'title'])

<div x-data="{ open:false }">
  <button type="button" @click="open=true" aria-haspopup="dialog" aria-controls="{{ $id }}">
    {{ $trigger ?? 'Abrir' }}
  </button>

  <div x-show="open" class="fixed inset-0 bg-black/50" aria-hidden="true"></div>

  <div x-show="open"
    id="{{ $id }}"
    role="dialog"
    aria-modal="true"
    aria-labelledby="{{ $id }}-title"
    class="fixed inset-0 flex items-center justify-center p-4"
    @keydown.escape.window="open=false"
  >
    <div class="bg-white rounded p-4 w-full max-w-lg">
      <h2 id="{{ $id }}-title" class="text-lg font-semibold">{{ $title }}</h2>
      <div class="mt-3">
        {{ $slot }}
      </div>
      <div class="mt-4 flex justify-end gap-2">
        <x-button variant="secondary" type="button" @click="open=false">Cerrar</x-button>
        {{ $actions ?? '' }}
      </div>
    </div>
  </div>
</div>

9. Estructura de directorios: mantenerse organizado a medida que crecen los componentes

Estructura recomendada:

resources/views/components/
  ui/
    button.blade.php
    link-button.blade.php
    notice.blade.php
    modal.blade.php
  form/
    field.blade.php
    input.blade.php
    select.blade.php
    checkbox.blade.php
    error-summary.blade.php

Reglas de nombres

  • Usa dominios como x-ui.button, x-form.input
  • Los componentes específicos de pantalla deben empezar como @include
  • Promueve al sistema de diseño solo las partes reutilizables universalmente

10. Gestión de variantes: no sobrecargues los props

Cuando los props crecen demasiado, los componentes se vuelven difíciles de usar.

  • Limita las opciones cambiables a variant, size y state
  • Permite sobrescrituras de class para excepciones
  • Si las excepciones crecen, crea un componente separado

Los componentes deben ser pequeños y fuertes, no infinitamente flexibles.


11. Accesibilidad como estándar: checklist de revisión

  • Roles correctos (button vs a)
  • label correctamente asociado a los inputs
  • Errores vinculados mediante aria-invalid y aria-describedby
  • Campos obligatorios indicados con texto, no solo con color
  • Anillos de foco visibles
  • Cambios de estado anunciados mediante role="status" / aria-live
  • Modales se cierran con Esc y atrapan el foco

12. Protección con tests: los componentes UI son propensos a regresiones

No pruebes todo E2E—concéntrate en áreas clave.

12.1 Tests Feature (estructura)

  • Aparecen resúmenes de error
  • Existen errores en sesión
  • Se muestran mensajes de éxito

12.2 Tests Dusk (interacción)

  • El resumen de error recibe el foco
  • aria-invalid="true" está presente
  • Los modales críticos abren/cierran y se cierran con Esc

13. Errores comunes y cómo evitarlos

  • Clases de botón inconsistentes entre páginas
    → Estandariza con x-button.
  • Formularios solo con placeholder sin etiquetas
    → Obliga el uso de x-field.
  • Errores mostrados solo en texto rojo
    → Encapsula mensajes y el enlace ARIA.
  • Modales solo para ratón
    → Estandariza Esc, foco y cierre por teclado.
  • Demasiados props
    → Limita variantes; divide componentes si es necesario.

14. Checklist (para distribución)

Diseño

  • [ ] Conjunto mínimo componentizado (botón / formulario / error / aviso / modal)
  • [ ] Roles separados (botón vs enlace)
  • [ ] Reglas de nombres de directorios definidas

Accesibilidad

  • [ ] Etiquetas vinculadas a inputs
  • [ ] Campos obligatorios indicados por texto
  • [ ] Errores usan aria-invalid y aria-describedby
  • [ ] Cambios de estado usan role="status"
  • [ ] Esc, foco y flujo de cierre del modal

Operación

  • [ ] Variantes mantenidas al mínimo
  • [ ] Excepciones no forzadas en componentes comunes
  • [ ] Flujos críticos cubiertos por tests Dusk

15. Conclusión

Construir un sistema de diseño con componentes Blade hace más que unificar la apariencia: encapsula accesibilidad y corrección dentro de partes reutilizables. Empezar solo con botones, entradas de formulario y resúmenes de error ya ofrece grandes beneficios. Ampliar a avisos y modales crea una UI tranquila y resiliente que no se pierde a medida que crecen las pantallas. Empieza pequeño y estandariza con cuidado desde los componentes más usados.


Enlaces de referencia

por greeden

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)