Orientación sobre los componentes web

Los componentes web son elementos personalizados, Shadow DOM y plantillas HTML: las primitivas de la plataforma para componentes reutilizables y agnósticos del framework cuyo lenguaje de marcado y estilos están encapsulados en el documento. Úsalos para sistemas de diseño, widgets incorporables y una IU independiente.

Las reglas que se indican a continuación son transversales. Recupera la guía específica para la tarea en cuestión. Cada una de ellas reitera el contexto que necesita y documenta las APIs y los errores que le son propios.

Decide si necesitas un elemento personalizado

Usa un elemento personalizado solo cuando necesites un comportamiento encapsulado, un ciclo de vida o una API reutilizable respaldada por JS en una etiqueta. No crees uno en los siguientes casos:

  • Un elemento HTML simple con CSS sería suficiente. Un <button> o <details> con diseño es más accesible y económico que una reimplementación.
  • La cosa es puramente de presentación, sin comportamiento. Una clase en un elemento nativo tiene un mejor rendimiento y se puede limitar con @layer o @scope.
  • Solo necesitas un alcance de estilo en el servidor sin interactividad; una hoja de estilo con alcance o un Shadow DOM declarativo (sin una actualización) pueden ser suficientes.

La reconstrucción de la semántica nativa (un <my-button> personalizado que recrea el enfoque, la activación y ARIA) es el uso inadecuado más común: heredas la accesibilidad de la plataforma de forma gratuita solo si reutilizas el elemento nativo. Cuando se justifica un wrapper de estilo <my-button> (por lo general, para hooks de estilo compartidos, lenguaje de marcado adicional o comportamiento que un elemento nativo no puede expresar), une el <button> real en lugar de volver a implementarlo (consulta Cómo aplicar diseño a componentes web para reenviar estilos a un elemento nativo interno).

La prueba consiste en determinar si necesitas un estado encapsulado, una API con secuencias de comandos o un comportamiento que el elemento nativo no puede expresar, no solo un aspecto diferente. Un botón de activación de Me gusta/Favorito cumple con los requisitos: tiene un estado de dominio bloqueado, expone ese estado como un hook de diseño para la página circundante y envía su propio evento change, algo que un <button> con diseño no te ofrece. Un botón que solo cambia de color cuando se presiona no lo hace: busca <button> + CSS. Cuando lo compiles, modela su estado con un estado o atributo personalizado, según corresponda (consulta Elementos personalizados).

Une la herramienta con la base de código

Si algo debería ser un elemento personalizado depende de lo que ya use el proyecto. Los agentes tienden a abstraer en exceso y a buscar un elemento nuevo con demasiada ansiedad. En caso de duda, la opción más ligera (elemento nativo, CSS, una plantilla de servidor) suele ser la correcta, y puedes promoverla a un elemento personalizado más adelante si la reutilización o el comportamiento realmente lo exigen. Heurísticas, en orden de prioridad aproximado:

  • La base de código ya tiene un modelo de componentes (React, Vue, Svelte, etcétera). Usa de forma predeterminada el componente de ese framework para la IU integrada en la app. Usa un elemento personalizado solo cuando el objeto deba escapar o sobrevivir al framework: una primitiva del sistema de diseño que se comparte entre apps o frameworks, un widget que se coloca en un lenguaje de marcado que el framework no controla o un resultado que debe funcionar como HTML sin formato (correo electrónico, CMS, fragmentos de SSR). No configures un sistema de componentes paralelo para las vistas de app comunes.
  • No hay herramientas, principalmente lenguaje de marcado estático. Prefiere HTML + CSS sin formato, o una plantilla o parcial de servidor. Un elemento personalizado se gana su lugar solo cuando realmente necesitas un comportamiento, un ciclo de vida o una API respaldada por JS en la etiqueta, no solo para compartir lenguaje de marcado, lo que hace un elemento de inclusión del servidor o <template> sin enviar ningún JS.
  • Generador de sitios estáticos con islas de interactividad. Renderiza el lenguaje de marcado de forma estática y reserva el elemento personalizado para la parte interactiva. Para "mucho lenguaje de marcado, poca interactividad", mantén el lenguaje de marcado en el DOM de luz (o el DOM de sombra declarativo) y deja que un pequeño elemento personalizado lo mejore de forma progresiva, en lugar de volver a renderizar todo desde JS.
  • En su lugar, podrías ampliar un elemento integrado. Por lo general, es preferible llevar un elemento nativo lo más lejos posible (CSS, algunos objetos de escucha, el pseudoelemento ocasional) que un elemento personalizado, ya que mantienes la accesibilidad y el comportamiento de la plataforma de forma gratuita. Pásate a un elemento personalizado cuando las soluciones alternativas dejen de ser mantenibles, es decir, cuando tengas que luchar contra el comportamiento integrado del elemento o cuando la experiencia de creación sea peor que la de una API de componente limpia. La necesidad de un poco de JS en un elemento integrado no es, por sí sola, un motivo para reemplazarlo.

Reglas transversales

Estas se aplican a todas las guías que se indican a continuación:

  • OBLIGATORIO: El nombre de la etiqueta debe estar en minúsculas con guiones medios y tener al menos un guion (p.ej., my-element). Esto es lo que distingue a los elementos personalizados de los integrados actuales y futuros.
  • OBLIGATORIO: Llama primero a super() en el constructor, antes de tocar this. Si lo llamas después de cualquier sentencia que lea o escriba this, se arrojará un error.
  • OBLIGATORIO: Usa elementos semánticos (<button>, <nav>, <ul>) dentro de las plantillas de sombra. La mayoría de los errores de accesibilidad en los árboles de sombra provienen de lenguaje de marcado no semántico; un espacio no cambia el rol de lo que proyecta.
  • OBLIGATORIO: Las referencias de id de ARIA (aria-labelledby, aria-describedby, aria-controls) no pueden cruzar un límite de sombra; el elemento de referencia y el id al que apunta deben estar en el mismo árbol.
  • Se prefiere attachShadow({ mode: 'open' }) para que las herramientas para desarrolladores, las secuencias de comandos de la página y la hidratación del servidor puedan llegar a element.shadowRoot. Usa 'closed' solo cuando debas ocultar la estructura interna de las secuencias de comandos de la página.
  • Evita los elementos integrados personalizados (extends HTMLButtonElement con is="…"); Safari se negó de forma permanente a enviarlos.

Matriz de guías de casos de uso

Identifica la tarea y recupera su guía:

Si necesitas… Recuperación
Define una nueva etiqueta semántica con comportamiento y ciclo de vida Elementos personalizados
Limitar los estilos y el lenguaje de marcado a un componente, y proyectar el contenido del consumidor en él Shadow DOM, plantillas y ranuras
Renderizar el árbol de sombra de un componente en el servidor (SSR) sin un destello de contenido sin diseño Renderiza previamente elementos personalizados
Diseño en el límite de sombra (temas, ::part, propiedades personalizadas) Cómo aplicar diseño a los componentes web
Envía el valor de un control personalizado a un <form> circundante Elementos personalizados asociados a formularios
Haz que un elemento personalizado sea accesible (roles, enfoque y ARIA en raíces de sombra) Componentes web accesibles