W3docs

Elementos Personalizados

Aprende a crear Custom Elements en JavaScript: define una clase, regístrala con customElements.define(), usa callbacks de ciclo de vida y extiende elementos integrados.

Los Custom Elements son uno de los pilares fundamentales de Web Components. Te permiten definir tus propias etiquetas HTML respaldadas por una clase JavaScript, extendiendo el vocabulario integrado del navegador con elementos reutilizables y autocontenidos que llevan su propia estructura, estilo y comportamiento.

Esta página cubre todo lo que necesitas para crear un elemento personalizado: cómo definirlo y registrarlo, los callbacks de ciclo de vida que el navegador invoca por ti, cómo reaccionar a cambios en los atributos, cómo extender elementos integrados, y las prácticas que mantienen tus componentes robustos y accesibles.

Dos tipos de elementos personalizados

La especificación define dos variantes, y la distinción afecta cómo los creas y los usas:

  • Autonomous custom elements (elementos personalizados autónomos) extienden el HTMLElement genérico y se usan como etiquetas completamente nuevas: <my-card></my-card>. Este es el caso más común.
  • Customized built-in elements (elementos integrados personalizados) extienden una clase integrada específica (como HTMLButtonElement) y se usan con el atributo is: <button is="fancy-button">. Heredan de forma gratuita la accesibilidad y el comportamiento del elemento anfitrión.

Una regla aplica a ambos: el nombre de la etiqueta debe contener un guión (my-card, no mycard). El guión es lo que le indica al analizador que la etiqueta es un elemento personalizado y evita colisiones con futuras etiquetas estándar.

Definir un Custom Element

Para crear un autonomous custom element, define una class que extienda la clase integrada HTMLElement, luego regístralo en el navegador con customElements.define(tagName, class). La clase encapsula el comportamiento del elemento; el registro lo conecta a un nombre de etiqueta.

Un patrón común es construir el DOM interno del elemento dentro de un shadow DOM para que su marcado y estilos queden aislados del resto de la página.

Ejemplo: Crear un Custom Element sencillo

<my-custom-element></my-custom-element>
<script>
  class MyCustomElement extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = `<p>Hello, World!</p>`;
    }
  }
  
  customElements.define('my-custom-element', MyCustomElement);
</script>

Este ejemplo define un elemento personalizado llamado my-custom-element que muestra "Hello, World!" dentro de un shadow DOM. Para usarlo, simplemente agrega <my-custom-element></my-custom-element> en cualquier parte de tu HTML. Ten en cuenta que los elementos personalizados no tienen forma de cierre automático — escribe siempre la etiqueta de cierre correspondiente.

Nota

Custom Elements v1 es compatible con todos los navegadores modernos (Chrome 54+, Firefox 52+, Safari 10.1+, Edge 79+). Verifica siempre la compatibilidad del navegador si tu objetivo son entornos heredados.

Callbacks de ciclo de vida

Los elementos personalizados disponen de un conjunto de callbacks de ciclo de vida que permiten a los desarrolladores ejecutar código en momentos específicos del ciclo de vida del elemento:

  • connectedCallback(): Se invoca cada vez que el elemento personalizado se añade a un elemento conectado al documento.
  • disconnectedCallback(): Se invoca cada vez que el elemento personalizado se desconecta del DOM del documento.
  • attributeChangedCallback(name, oldValue, newValue): Se invoca cada vez que uno de los atributos del elemento personalizado se añade, elimina o cambia.
  • adoptedCallback(): Se invoca cada vez que el elemento personalizado se mueve a un nuevo documento.
CallbackCuándo se dispara
connectedCallback()El elemento se añade al DOM
disconnectedCallback()El elemento se elimina del DOM
attributeChangedCallback(name, oldValue, newValue)Un atributo observado cambia
adoptedCallback()El elemento se mueve a un nuevo documento

Un modelo mental útil: el constructor se ejecuta una sola vez cuando se crea la instancia del elemento (antes de que esté en el DOM, por lo que no debe acceder a atributos ni a hijos), mientras que connectedCallback puede ejecutarse varias veces si el elemento se añade, se elimina y se vuelve a añadir. Realiza la configuración dependiente del DOM en connectedCallback, y limpia los listeners o temporizadores en disconnectedCallback para evitar fugas de memoria.

Ejemplo: Usar callbacks de ciclo de vida

<lifecycle-element></lifecycle-element>
<script>
class LifecycleElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        #status {
          color: blue;
          font-weight: bold;
        }
      </style>
      <p>Lifecycle Element</p>
      <p id="status">Element not connected</p>
    `;
  }

  connectedCallback() {
    this.shadowRoot.getElementById('status').textContent = 'Element connected to the page.';
  }

  disconnectedCallback() {
    this.shadowRoot.getElementById('status').textContent = 'Element disconnected from the page.';
  }
}

customElements.define('lifecycle-element', LifecycleElement);
</script>

Atributos y propiedades

Los elementos personalizados pueden tener atributos y propiedades para gestionar su estado y comportamiento. Los atributos se establecen directamente en HTML y siempre son string, mientras que las propiedades se establecen en el objeto DOM del elemento y pueden ser de cualquier tipo de dato.

El detalle clave es attributeChangedCallback: solo se dispara para los atributos que se listen explícitamente en el getter static get observedAttributes() del elemento. Si un atributo no está en ese array, modificarlo no desencadena ningún callback. Una convención habitual es exponer una propiedad getter/setter que simplemente refleje un atributo, de modo que el código JavaScript y el HTML permanezcan sincronizados.

Ejemplo: Gestionar atributos y propiedades

<attribute-element id="element" data-content="Initial content"></attribute-element>
<button onclick="buttonClicked()">Click to change attribute</button>
<script>
  class AttributeElement extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = `<p>Attribute Example: <span id="content"></span></p>`;
    }
  
    static get observedAttributes() {
      return ['data-content'];
    }
  
    attributeChangedCallback(name, oldValue, newValue) {
      if (name === 'data-content') {
        this.shadowRoot.getElementById('content').textContent = newValue;
      }
    }
  
    set content(value) {
      this.setAttribute('data-content', value);
    }
  
    get content() {
      return this.getAttribute('data-content');
    }
  }
  
  customElements.define('attribute-element', AttributeElement);

  function buttonClicked() {
    alert('button clicked!');
    const ourCustomElement = document.getElementById('element');
    ourCustomElement.content = 'New content';
  }
</script>

Aquí, el attribute-element actualiza su contenido en función del atributo data-content. La propiedad content proporciona una forma cómoda de obtener y establecer este atributo mediante programación.

Extender elementos integrados

Los customized built-in elements extienden una clase integrada específica y se usan con el atributo is. La gran ventaja es que heredan la semántica y la accesibilidad del elemento anfitrión — un <button is="fancy-button"> sigue siendo un botón real para el teclado y los lectores de pantalla.

Ejemplo: Extender un elemento integrado

<button is="fancy-button">Click me!</button>
<script>
  class FancyButton extends HTMLButtonElement {
    constructor() {
      super();
      this.addEventListener('click', () => {
        alert('Fancy button clicked!');
      });
    }
  }
  
  customElements.define('fancy-button', FancyButton, { extends: 'button' });
</script>

Aquí, fancy-button extiende el elemento estándar <button>, añadiendo un mensaje de alerta cuando se hace clic en el botón. El tercer argumento de customElements.define{ extends: 'button' } — le indica al navegador a qué etiqueta se aplica este elemento personalizado.

Nota

Safari no es compatible con los customized built-in elements (la forma is=). Para una mayor compatibilidad, prefiere los autonomous custom elements y reimplementa la accesibilidad que necesites, o carga un polyfill.

Buenas prácticas para elementos personalizados

  1. Usa Shadow DOM: Encapsula siempre la estructura interna y los estilos de tu elemento personalizado usando el Shadow DOM.
  2. Define APIs claras: Proporciona APIs claras e intuitivas para tus elementos personalizados a través de atributos y propiedades bien documentados.
  3. Gestión del ciclo de vida: Gestiona correctamente los callbacks del ciclo de vida del elemento para garantizar un comportamiento robusto y evitar fugas de memoria.
  4. Accesibilidad: Asegúrate de que tus elementos personalizados sean accesibles incluyendo los roles y propiedades ARIA adecuados.
  5. Pruebas: Prueba exhaustivamente tus elementos personalizados en diferentes navegadores y entornos para garantizar la compatibilidad y la estabilidad.

Conclusión

Los Custom Elements ofrecen una forma poderosa de extender HTML, permitiendo la creación de componentes reutilizables y encapsulados con comportamiento personalizado. Aprovechando las características de los elementos personalizados, incluidos los callbacks de ciclo de vida, atributos, propiedades y el Shadow DOM, los desarrolladores pueden crear aplicaciones web sofisticadas y mantenibles.

Empieza a experimentar con elementos personalizados en tus proyectos hoy mismo y descubre nuevas posibilidades para el desarrollo web. Los ejemplos que se presentan aquí son solo el principio — úsalos como base para crear tus propios Custom Elements innovadores.

Temas relacionados

Práctica

Práctica
¿Cuáles de las siguientes afirmaciones sobre los Custom Elements en JavaScript son verdaderas?
¿Cuáles de las siguientes afirmaciones sobre los Custom Elements en JavaScript son verdaderas?
Was this page helpful?