StackPractices
intermediate Por Mathias Paulenko

Web Components — Custom Elements, Shadow DOM y Templates

Guía práctica de Web Components: crear elementos personalizables reutilizables, encapsular estilos con Shadow DOM y componer con templates HTML.

Temas: frontend

Visión General

Web Components son un conjunto de APIs nativas del navegador para crear elementos HTML reutilizables y encapsulados. Funcionan en cualquier framework — o sin ninguno — y proporcionan verdadera encapsulación de estilos y DOM mediante Shadow DOM. A continuación: las tres tecnologías principales: Custom Elements, Shadow DOM y HTML Templates, con ejemplos prácticos que puedes usar hoy.

Cuándo Usar

  • For alternatives, see Feature Flags: Progressive Release and Safe Experimentation.

  • Necesitas elementos UI reutilizables compartidos entre diferentes proyectos o frameworks

  • Quieres encapsulación de estilos sin CSS-in-JS o convenciones de nomenclatura BEM

  • Estás construyendo un sistema de diseño que debe funcionar en React, Vue, Angular o JS vanilla

  • Necesitas extender elementos HTML nativos con comportamiento personalizado

  • Quieres componentes independientes del framework para mantenibilidad a largo plazo

Las Tres Tecnologías

TecnologíaPropósitoEstándar
Custom ElementsDefinir nuevas etiquetas HTML con JavaScriptCustom Elements v1
Shadow DOMEncapsular DOM y estilos dentro de un componenteShadow DOM v1
HTML TemplatesDeclarar fragmentos de marcado reutilizablesHTML Template Element

Custom Elements

Custom Elements Autónomos

Crea etiquetas HTML completamente nuevas.

class UserCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
  }

  render() {
    const name = this.getAttribute('name') || 'Anónimo';
    const role = this.getAttribute('role') || 'Usuario';
    
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: block; padding: 1rem; border: 1px solid #e5e7eb; border-radius: 0.5rem; }
        .name { font-weight: 600; color: #111827; }
        .role { color: #6b7280; font-size: 0.875rem; }
      </style>
      <div class="name">${name}</div>
      <div class="role">${role}</div>
    `;
  }

  static get observedAttributes() {
    return ['name', 'role'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) this.render();
  }
}

customElements.define('user-card', UserCard);
<!-- Uso -->
<user-card name="Alice Chen" role="Senior Engineer"></user-card>
<user-card name="Bob Smith" role="Product Manager"></user-card>

Customized Built-in Elements

Extiende elementos HTML existentes con nuevo comportamiento.

class ConfirmButton extends HTMLButtonElement {
  constructor() {
    super();
    this.addEventListener('click', (e) => {
      if (!confirm(this.getAttribute('confirm-message') || 'Estás seguro?')) {
        e.preventDefault();
      }
    });
  }
}

customElements.define('confirm-button', ConfirmButton, { extends: 'button' });
<!-- Uso mediante atributo is="" -->
<button is="confirm-button" confirm-message="Eliminar este archivo permanentemente?">
  Eliminar
</button>

Shadow DOM

Encapsulación

Shadow DOM aísla el DOM y CSS de un componente del resto de la página.

class StyledCounter extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      <style>
        /* Enfocado solo a este componente */
        button {
          background: #3b82f6;
          color: white;
          border: none;
          padding: 0.5rem 1rem;
          border-radius: 0.25rem;
          cursor: pointer;
        }
        button:hover { background: #2563eb; }
        span { margin-left: 0.5rem; font-weight: 600; }
      </style>
      <button id="inc">+</button>
      <span id="count">0</span>
    `;
    
    this.count = 0;
    shadow.getElementById('inc').addEventListener('click', () => {
      this.count++;
      shadow.getElementById('count').textContent = this.count;
    });
  }
}

customElements.define('styled-counter', StyledCounter);

Slots

Los slots permiten inyectar contenido en el Shadow DOM de un componente.

class AlertBox extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' }).innerHTML = `
      <style>
        :host { display: block; padding: 1rem; border-radius: 0.5rem; }
        :host([type="error"]) { background: #fef2f2; border: 1px solid #fca5a5; }
        :host([type="warning"]) { background: #fffbeb; border: 1px solid #fcd34d; }
        :host([type="success"]) { background: #f0fdf4; border: 1px solid #86efac; }
        ::slotted(h3) { margin: 0 0 0.5rem; font-size: 1rem; }
        ::slotted(p) { margin: 0; color: #4b5563; }
      </style>
      <slot name="title"></slot>
      <slot></slot>
    `;
  }
}

customElements.define('alert-box', AlertBox);
<!-- Uso con slots nombrados -->
<alert-box type="error">
  <h3 slot="title">Conexión Fallida</h3>
  <p>No se puede alcanzar el servidor. Por favor verifica tu red e intenta de nuevo.</p>
</alert-box>

HTML Templates

Los templates declaran marcado reutilizable que no se renderiza hasta clonarse.

<template id="user-row-template">
  <tr>
    <td class="name"></td>
    <td class="email"></td>
    <td><button class="delete">Remover</button></td>
  </tr>
</template>
function createUserRow(user) {
  const template = document.getElementById('user-row-template');
  const clone = template.content.cloneNode(true);
  
  clone.querySelector('.name').textContent = user.name;
  clone.querySelector('.email').textContent = user.email;
  clone.querySelector('.delete').addEventListener('click', () => removeUser(user.id));
  
  return clone;
}

// Agregar a la tabla
document.querySelector('#users tbody').appendChild(createUserRow({
  name: 'Alice Chen',
  email: 'alice@example.com'
}));

Ciclo de Vida del Componente

CallbackCuándo Se DisparaUso Común
constructor()Elemento creadoInicializar estado, adjuntar shadow root
connectedCallback()Insertado en el DOMRenderizar, obtener datos, agregar event listeners
disconnectedCallback()Removido del DOMLimpiar temporizadores, event listeners, suscripciones
attributeChangedCallback()Cambios en atributos observadosRe-renderizar, validar, actualizar estado interno
adoptedCallback()Movido a un nuevo documentoRaramente usado; limpiar y re-inicializar

Integración con Frameworks

Web Components funcionan en cualquier framework.

// React
import 'my-ui-library';

function App() {
  return (
    <div>
      <user-card name="Alice" role="Engineer" />
      <alert-box type="success">
        <h3 slot="title">Guardado</h3>
        <p>Tus cambios han sido guardados.</p>
      </alert-box>
    </div>
  );
}
<!-- Vue -->
<template>
  <user-card :name="user.name" :role="user.role" />
</template>

<script>
import 'my-ui-library';
export default { props: ['user'] };
</script>

Errores Comunes

  • Olvidar manejar cambios de atributos — los atributos observados deben disparar re-renderizado
  • Usar innerHTML en el elemento host — siempre usa Shadow DOM para el marcado del componente
  • No limpiar en disconnectedCallback() — fugas de memoria de event listeners huérfanos
  • Asumir que constructor corre después de inserción en DOM — corre en creación; el DOM puede no existir aún
  • Faltar polyfills para navegadores antiguos — Edge 18 e IE11 necesitan el polyfill de webcomponentsjs

Troubleshooting

  • Component does not re-render: verify state reference, props, and memoization. A mutated object can bypass change detection.
  • Style does not apply in production: check that CSS is loaded, class names are not mangled, and specificity wins. Purge unused styles carefully.
  • Build fails after dependency update: read the changelog, pin versions, and clean the lock file.
  • Accessibility audit fails: add labels, landmarks, focus management, and color contrast.
  • Hydration mismatch: ensure server and client render the same initial HTML. random, or window during SSR.

Notas de Producción

  • Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
  • Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
  • Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
  • Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.

Puntos Clave

  • Aplica web components — custom elements, shadow dom y templates cuando necesites una solución práctica para tu caso de uso.
  • Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
  • Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
  • Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.

Errores Comunes en Producción

  • Tratar la guía como un checklist para completar una vez en lugar de una práctica por evolucionar.
  • Adoptar cada recomendación de golpe en lugar de comenzar con un cambio medido.
  • Saltar la evaluación de madurez e imponer prácticas avanzadas a un equipo no preparado.
  • No actualizar runbooks y expectativas de guardia al introducir nuevas prácticas.
  • Ignorar datos reales de incidentes al priorizar qué partes de la guía aplicar primero.
  • No asignar un responsable que revise decisiones trimestralmente.
  • Copiar ejemplos sin adaptarlos a las herramientas y restricciones reales del equipo.
  • Olvidar medir resultados antes de agregar la siguiente mejora.

Preguntas frecuentes

¿Cómo empiezo con esto en un proyecto existente?

Empieza con una parte pequeña y aislada de tu codebase. Aplica los conceptos de esta guía a un módulo o servicio. Mide el impacto, luego expande a otras áreas.

¿Qué herramientas necesito?

Las herramientas mencionadas throughout esta guía se listan en cada sección. La mayoría son open-source y ampliamente adoptadas. Consulta los recursos relacionados para instrucciones de setup.

¿Cómo mido el éxito después de implementar esto?

Define métricas claras antes de empezar: benchmarks de rendimiento, tasas de error o indicadores de mantenibilidad. Compara antes y después. Itera basándote en datos, no en suposiciones.