Tot el contingut que <task-card> ha mostrat fins ara —el títol, la insígnia d'estat, l'avís d'urgència— el genera el propi component, dins del seu render(), a partir de les seves propietats reactives. Però hi ha un cas diferent, molt habitual en interfícies reals, que encara no s'ha cobert: un component que rep contingut ja construït des de fora i necessita col·locar-lo en un punt concret de la seva propia plantilla interna. Aquesta lliçó presenta l'element <slot>, el mecanisme estàndard dels Web Components per a aquest escenari, explica com estilitzar aquest contingut distribuït des de dins amb ::slotted(), i l'aplica creant <user-avatar>, un nou component de TaskFlow que <task-card> usarà per mostrar la persona assignada a cada tasca.

Contingut

  1. El problema: contingut que ve de fora, no de les propietats
  2. Què és un <slot> i com distribueix contingut
  3. Slot per defecte davant de slots amb nom
  4. Estilitzant contingut distribuït amb ::slotted()
  5. Les limitacions de ::slotted()
  6. Creant <user-avatar>
  7. Usant <user-avatar> dins de <task-card>
  8. Tancament del mòdul: cap a la comunicació entre components

  1. El problema: contingut que ve de fora, no de les propietats

Fins ara, cada vegada que <task-card> ha necessitat mostrar informació variable, aquesta informació ha arribat com una propietat reactiva de tipus simple: una cadena de text (titulo), un número (prioridad), un booleà (urgente). El patró sempre ha estat el mateix: la dada entra com a propietat, render() la interpola dins d'una plantilla que el propi component controla per complet.

Però imagina ara que es vol mostrar, dins de cada targeta, un petit indicador visual de la persona assignada a la tasca: una imatge, o potser només les seves inicials sobre un fons de color. Es podria resoldre amb més propietats (nombreAsignado, imagenAsignado...), però hi ha una alternativa diferent, més flexible, quan el que es vol passar no és una dada simple sinó contingut HTML ja construït, potencialment diferent segons el cas: a vegades una imatge, a vegades només text, a vegades una icona. Aquest és exactament l'escenari per al qual existeixen els <slot>.

  1. Què és un <slot> i com distribueix contingut

Un <slot> és un element especial, reconegut de forma nativa pel navegador (no és una invenció de Lit; forma part de l'estàndard de Web Components des del seu origen), que es col·loca dins del Shadow DOM d'un component i actua com un "buit": qualsevol contingut que s'escrigui entre les etiquetes d'obertura i tancament de l'element personalitzat, al DOM lleuger (fora del shadow root), es distribueix automàticament dins d'aquest buit quan el navegador compon l'arbre final que es mostra en pantalla.

import { LitElement, html, css } from 'lit';

class UserAvatar extends LitElement {
  static styles = css`
    :host {
      display: inline-block;
    }
  `;

  render() {
    return html`<div class="avatar"><slot></slot></div>`;
  }
}

customElements.define('user-avatar', UserAvatar);
<user-avatar>AC</user-avatar>

En aquest exemple, el text AC, escrit entre les etiquetes <user-avatar> i </user-avatar> a l'HTML normal (fora de qualsevol shadow root), no forma part de la plantilla de render() en cap sentit literal: render() no sap, ni necessita saber, quin contingut concret es distribuirà. El que passa és que el navegador, en renderitzar visualment l'arbre final, "projecta" aquest text AC dins de l'<slot> que apareix al shadow root, de manera que en pantalla apareix exactament com si AC estigués escrit directament dins del <div class="avatar">, encara que a l'arbre lògic del DOM el text continuï pertanyent, en realitat, a l'element lleuger <user-avatar>, no al shadow root.

Aquest mecanisme és completament diferent de passar una propietat: no hi ha cap static properties implicat, ni cap conversió de tipus, ni cap reactivitat de Lit en joc. El slot és, literalment, una tècnica de composició visual del propi navegador, que Lit no reinventa sinó que exposa amb total naturalitat perquè el seu render() genera HTML normal, capaç d'incloure qualsevol element estàndard, <slot> inclòs.

  1. Slot per defecte davant de slots amb nom

L'exemple anterior usa un slot per defecte (<slot></slot>, sense cap atribut name): recull tot el contingut distribuïble que no estigui marcat explícitament per a un altre slot. Un component pot tenir, a més, diversos slots amb nom, cadascun dels quals recull només el contingut que, des de fora, es marqui amb l'atribut slot corresponent:

render() {
  return html`
    <div class="avatar">
      <slot name="imagen"></slot>
      <slot></slot>
    </div>
  `;
}
<user-avatar>
  <img slot="imagen" src="ana.jpg" alt="Ana" />
  AC
</user-avatar>

Aquí, l'<img> amb slot="imagen" es distribueix dins de l'<slot name="imagen">, mentre que el text AC (que no porta cap atribut slot, i per tant no està destinat a cap slot amb nom concret) es distribueix dins del slot per defecte. Un component pot tenir tants slots amb nom com necessiti, permetent repartir diferents peces de contingut distribuït en diferents punts de la plantilla interna, mentre que el slot per defecte (com a molt un per component, sense name) recull tota la resta. Per a <user-avatar>, com es veurà a l'apartat 6, n'hi ha prou amb un únic slot per defecte, ja que només hi ha un tipus de contingut a distribuir alhora: o bé les inicials, o bé una imatge, mai totes dues.

  1. Estilitzant contingut distribuït amb ::slotted()

El contingut que arriba a través d'un <slot> planteja una pregunta natural: des d'on se li pot donar estil? Com aquest contingut, a l'arbre lògic del DOM, continua pertanyent al document exterior (al DOM lleuger de <user-avatar>, no al seu shadow root), el CSS declarat dins de static styles no l'aconsegueix amb un selector normal com img { ... }: aquella regla només afecta elements <img> que estiguin realment dins del shadow root, i l'<img> distribuïda, encara que es vegi visualment en aquell lloc, no ho està en termes de l'arbre lògic del DOM.

Per a aquest cas concret, CSS ofereix un pseudo-element dedicat: ::slotted(), que permet seleccionar, des de dins del shadow root, el contingut que un <slot> està distribuint en aquell moment.

static styles = css`
  ::slotted(img) {
    border-radius: 50%;
    width: 2rem;
    height: 2rem;
    object-fit: cover;
  }
`;

Aquesta regla selecciona qualsevol element <img> que estigui essent distribuït per algun <slot> del component, i li aplica un border-radius: 50% (per donar-li forma circular, un tractament típic d'un avatar d'usuari) juntament amb una mida fixa. Nota la sintaxi: ::slotted() és un pseudo-element (amb dos dos-punts, igual que ::before o ::after del CSS estàndard), no una pseudo-classe com :host(), i rep entre parèntesis un selector que descriu quin contingut distribuït seleccionar.

  1. Les limitacions de ::slotted()

::slotted() és deliberadament limitat en el que pot seleccionar, i convé conèixer aquesta limitació de bestreta per no perdre temps depurant un selector que, simplement, no està suportat per l'estàndard:

  • Només admet selectors simples, aplicats directament sobre l'element distribuït de nivell superior: una etiqueta (::slotted(img)), una classe (::slotted(.avatar-imagen)), un atribut (::slotted([data-tipo="imagen"])). No admet combinadors de descendència: ::slotted(div span) no és vàlid i el navegador l'ignora directament.
  • No permet "entrar" dins del contingut distribuït: si el contingut distribuït és un element amb fills propis (per exemple, un <div> que al seu torn conté un <span>), ::slotted() només pot seleccionar aquell <div> de nivell superior, mai l'<span> que hi ha dins d'ell. La regla ::slotted(div) span { ... }, amb un combinador després del pseudo-element, tampoc és vàlida.
  • Només selecciona els nodes distribuïts directament pel slot, no qualsevol element aniuat més profundament a la jerarquia del contingut distribuït.

Aquesta limitació no és un descuit: és una decisió deliberada de l'estàndard per mantenir la mateixa filosofia d'encapsulació que s'ha defensat en tot aquest mòdul. Si ::slotted() permetés selectors de descendència arbitraris, un component podria acabar donant estil a l'estructura interna d'un contingut que, en última instància, no controla ni coneix de bestreta (el contingut distribuït el decideix qui usa el component, no el component mateix), el que trencaria la mateixa separació de responsabilitats que el Shadow DOM persegueix a la resta d'aquest mòdul.

A la pràctica, aquesta limitació empeny cap a una convenció sensata: mantenir el contingut que es distribueix a través d'un slot raonablement simple (una imatge solta, un fragment de text, una icona), deixant qualsevol estructura interna més complexa per a plantilles generades pel propi component en lloc de contingut distribuït.

  1. Creant <user-avatar>

Amb la teoria coberta, és el moment de construir <user-avatar> complet, com a nou component de TaskFlow: un component petit i reutilitzable, pensat per mostrar la persona assignada a una tasca, ja sigui amb una imatge o amb les seves inicials com a alternativa quan no hi ha imatge disponible.

// src/components/user-avatar.js
import { LitElement, html, css } from 'lit';

class UserAvatar extends LitElement {
  static properties = {
    nombre: { type: String },
  };

  static styles = css`
    :host {
      display: inline-block;
      --tamano-avatar: 2rem;
    }

    .avatar {
      width: var(--tamano-avatar);
      height: var(--tamano-avatar);
      border-radius: 50%;
      background-color: #cbd5e1;
      color: #1f2933;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 0.8rem;
      font-weight: bold;
      overflow: hidden;
    }

    ::slotted(img) {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
  `;

  constructor() {
    super();
    this.nombre = '';
  }

  render() {
    return html`
      <div class="avatar" title="${this.nombre}">
        <slot>${this.iniciales()}</slot>
      </div>
    `;
  }

  iniciales() {
    if (!this.nombre) {
      return '?';
    }
    return this.nombre
      .split(' ')
      .map((palabra) => palabra.charAt(0).toUpperCase())
      .slice(0, 2)
      .join('');
  }
}

customElements.define('user-avatar', UserAvatar);

Diversos detalls mereixen explicació. Primer, <user-avatar> combina, dins d'una mateixa plantilla, el ja vist en aquest mòdul i en l'anterior: una propietat reactiva nombre (de tipus String, seguint el patró del mòdul 3), una variable CSS --tamano-avatar declarada a :host amb el seu valor per defecte (seguint el patró de theming de la lliçó anterior), i ara un <slot> amb contingut de recanvi entre les seves etiquetes d'obertura i tancament.

Aquell contingut de recanvi —<slot>${this.iniciales()}</slot>— és un detall important de l'estàndard de <slot>: qualsevol contingut escrit directament dins de les etiquetes <slot>...</slot> a la plantilla del component es mostra només quan no hi ha cap contingut distribuït des de fora que ocupi aquell slot. Si qui usa <user-avatar> no escriu res entre les seves etiquetes (<user-avatar nombre="Ana Costa"></user-avatar>), el slot està buit i el navegador mostra automàticament el contingut de recanvi: en aquest cas, el resultat de this.iniciales(), que calcula les inicials del nom a partir de la propietat reactiva nombre (aprofitant aquí sí una propietat normal, perquè les inicials són una dada derivada de text simple, no contingut HTML complex). Si, en canvi, es distribueix una imatge des de fora, aquesta imatge substitueix per complet el contingut de recanvi.

La regla ::slotted(img) dóna a qualsevol imatge distribuïda una mida que omple completament el cercle de l'avatar, retallant-la de forma proporcional amb object-fit: cover, exactament l'ús de ::slotted() presentat a l'apartat 4.

  1. Usant <user-avatar> dins de <task-card>

Amb <user-avatar> ja construït, s'incorpora dins de <task-card>, tant en forma d'inicials (sense contingut distribuït, usant el nom com a propietat) com, opcionalment, amb una imatge real quan la tasca la tingui disponible:

import { LitElement, html, css } from 'lit';
import { estilosCompartidos } from '../styles/shared-styles.js';
import './user-avatar.js';

class TaskCard extends LitElement {
  static properties = {
    titulo: { type: String },
    estado: { type: String },
    prioridad: { type: Number },
    urgente: { type: Boolean },
    asignadoA: { type: String },
    asignadoImagen: { type: String },
    expandida: { state: true },
    fechaLimite: { converter: conversorDeFecha, attribute: 'fecha-limite' },
  };

  static styles = [
    estilosCompartidos,
    css`
      /* ...reglas ya vistas en las lecciones anteriores... */

      .cabecera {
        display: flex;
        align-items: center;
        gap: 0.5rem;
      }
    `,
  ];

  constructor() {
    super();
    this.titulo = 'Tarea sin título';
    this.estado = 'pendiente';
    this.prioridad = 3;
    this.urgente = false;
    this.asignadoA = '';
    this.asignadoImagen = '';
    this.expandida = false;
    this.fechaLimite = null;
  }

  renderAvatar() {
    if (this.asignadoImagen) {
      return html`
        <user-avatar nombre="${this.asignadoA}">
          <img src="${this.asignadoImagen}" alt="${this.asignadoA}" />
        </user-avatar>
      `;
    }
    return html`<user-avatar nombre="${this.asignadoA}"></user-avatar>`;
  }

  render() {
    return html`
      <article @click="${this.alternarExpandida}">
        <div class="cabecera">
          ${this.renderAvatar()}
          <h3>${this.titulo}</h3>
        </div>
        ${this.renderInsigniaEstado()}
        <p>Prioridad: ${this.prioridad}</p>
        ${this.renderFechaLimite()}
        ${this.urgente && html`<p class="aviso">⚠ Urgente</p>`}
        ${this.expandida
          ? html`<div class="detalle"><p>Estado interno: la tarjeta está expandida.</p></div>`
          : ''}
      </article>
    `;
  }
}

customElements.define('task-card', TaskCard);

renderAvatar() decideix, segons si la tasca té o no una imatge associada (una nova propietat asignadoImagen, de tipus String), si es distribueix un <img> dins de <user-avatar> o si es deixa el slot buit perquè apareguin les inicials calculades automàticament. En tots dos casos es passa nombre com a atribut normal, ja que <user-avatar> el necessita tant per calcular les inicials de recanvi com per a l'atribut title d'accessibilitat vist a la seva plantilla. Aquest patró —un component que decideix, segons les seves pròpies dades, quin contingut distribuir dins d'un altre component fill— combina, en una sola peça de TaskFlow, tot el vist fins ara: propietats reactives i dades (mòdul 3), estils encapsulats i compartits, variables CSS de theming, i ara slots amb contingut distribuït i estilitzat mitjançant ::slotted().

  1. Tancament del mòdul: cap a la comunicació entre components

Amb aquesta lliçó es completa el mòdul 4, "Estils en Components Lit". El recorregut ha anat de la base a allò més avançat: primer, la doble frontera d'encapsulació que aixeca el Shadow DOM i la forma correcta de declarar CSS amb static styles i css; després, com evitar la duplicació extraient estils compartits entre diversos components; a continuació, les variables CSS com l'única esquerda deliberada d'aquesta frontera, i el seu ús com a mecanisme de theming; i, en aquesta última lliçó, els slots com a forma de rebre i integrar contingut distribuït des de fora, amb ::slotted() com a eina, limitada però suficient, per donar-li estil des de dins.

TaskFlow, com a resultat de tot el mòdul, ha fet un salt visual complet: <task-card> té ja una aparença acurada, amb vores, tipografia i colors d'estat configurables mitjançant variables CSS; <task-list> organitza les targetes en una columna ordenada compartint la mateixa base tipogràfica; i un nou component, <user-avatar>, s'integra dins de cada targeta per mostrar, mitjançant contingut distribuït amb slots, la persona assignada a cada tasca, amb imatge real o amb inicials de recanvi. Les dades ja eren reactives des del mòdul 3; ara, a més, es veuen bé.

No obstant això, si has estat seguint el curs provant aquests exemples al navegador, és fàcil notar que TaskFlow segueix essent, en el fons, una interfície passiva: el clic sobre una targeta només alterna un estat intern visual (expandida, vist al mòdul 3), i no existeix cap forma real de que l'usuari marqui una tasca com a completada, canviï la seva prioritat, o de que <task-card> avisi <task-list> de que quelcom ha canviat. Ara que TaskFlow es veu bé, falta que els seus components es comuniquin entre ells de veritat: que un clic sobre una targeta pugui, per exemple, notificar al seu component pare que l'usuari vol canviar l'estat d'una tasca. Aquesta és exactament la tasca del mòdul 5, "Esdeveniments i Comunicació entre Components".

Errors Comuns i Consells

  • Esperar que un selector normal, com img { ... }, aconsegueixi el contingut distribuït: com es va explicar a l'apartat 4, un selector normal dins de static styles només afecta elements que estiguin realment dins del shadow root; el contingut distribuït per un <slot> necessita ::slotted() per poder rebre estil des de dins del component.
  • Intentar usar un selector de descendència dins de ::slotted(): com es va detallar a l'apartat 5, ::slotted(div span) o ::slotted(div) span no són selectors vàlids; ::slotted() només admet un selector simple aplicat al node distribuït de nivell superior, mai als seus descendents.
  • Oblidar que el contingut de recanvi d'un <slot> només apareix quan el slot està realment buit: si es distribueix qualsevol contingut, encara que sigui un espai en blanc o un comentari HTML no buit, el contingut de recanvi escrit dins de <slot>...</slot> deixa de mostrar-se; convé comprovar, si el contingut de recanvi no apareix quan s'esperava, que realment no s'estigui distribuint res des de fora.
  • Confondre l'atribut slot="nombre" d'un element distribuït amb l'atribut name del propi <slot>: l'<slot name="imagen"> de la plantilla interna defineix el nom del buit; l'element que es vol distribuir en aquell buit concret necessita l'atribut slot="imagen" (mateix valor, atribut diferent) a l'HTML exterior, com es va mostrar a l'apartat 3. Confondre tots dos atributs és una font freqüent de "el slot amb nom no rep res".

Exercicis

  1. Afegeix a <user-avatar> un segon slot amb nom, estado-conexion, pensat per distribuir un petit indicador (per exemple, un <span> amb un punt de color) que mostri si la persona està connectada, i col·loca'l a la plantilla al costat del slot per defecte ja existent.
  2. Escriu una regla ::slotted(span) dins de static styles de <user-avatar> que doni a l'indicador de connexió de l'exercici anterior una mida petita i forma circular, i explica per què aquesta regla no afectaria, per exemple, un <span> aniuat dins d'un <div> que també es distribuís al mateix slot.
  3. Explica amb les teves pròpies paraules, basant-te en els apartats 1 i 6, per què té més sentit usar un <slot> per a l'avatar de la persona assignada (que pot ser una imatge o un text d'inicials) que declarar dues propietats reactives separades, mostrarImagen i urlImagen, i decidir a render() quina de les dues usar.

Solucions

render() {
  return html`
    <div class="avatar" title="${this.nombre}">
      <slot>${this.iniciales()}</slot>
    </div>
    <slot name="estado-conexion"></slot>
  `;
}
<user-avatar nombre="Ana Costa">
  <span slot="estado-conexion" class="punto-conectado"></span>
</user-avatar>
::slotted(span) {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  display: inline-block;
}

Aquesta regla afecta únicament l'<span> que es distribueix directament a un slot de <user-avatar> (el node de nivell superior del contingut distribuït); si aquell <span> estigués aniuat dins d'un <div> que fos, al seu torn, l'element realment distribuït al slot, ::slotted(span) no l'aconseguiria, perquè, com es va explicar a l'apartat 5, ::slotted() no pot "entrar" a l'estructura interna del contingut distribuït, només seleccionar el propi node de nivell superior.

  1. Com s'explica a l'apartat 1, el que varia aquí no és una dada simple (un text, un número, un booleà) sinó la pròpia naturalesa del contingut a mostrar: a vegades una imatge completa amb el seu propi src i alt, a vegades un simple text d'inicials. Modelar-ho amb dues propietats (mostrarImagen, urlImagen) obligaria <user-avatar> a conèixer de bestreta tots els tipus de contingut possibles i a construir ell mateix l'etiqueta <img> o el text segons un booleà, mentre que un <slot> trasllada aquella decisió a qui usa el component, que pot distribuir literalment qualsevol HTML vàlid (una imatge, una icona SVG, o res en absolut, deixant les inicials de recanvi) sense que <user-avatar> necessiti cap propietat addicional ni cap lògica nova per contemplar aquell cas.

Conclusió

En aquesta última lliçó del mòdul 4 has après què és un <slot> i com distribueix contingut HTML des de fora del shadow root d'un component, la diferència entre el slot per defecte i els slots amb nom, i com donar estil a aquest contingut distribuït des de dins amb ::slotted(), amb les seves limitacions deliberades de selectors simples sense descendència. Has aplicat tot això creant <user-avatar>, un nou component de TaskFlow amb contingut de recanvi calculat a partir d'una propietat reactiva i un slot capaç de rebre tant inicials com una imatge real, i l'has integrat dins de <task-card> per mostrar la persona assignada a cada tasca.

Amb això es tanca el mòdul "Estils en Components Lit": TaskFlow té ja un aspecte visual complet, coherent i personalitzable mitjançant variables CSS, amb un component addicional, <user-avatar>, que amplia el sistema mitjançant contingut distribuït. Ara que es veu bé, falta que els components es comuniquin entre ells: al mòdul 5, "Esdeveniments i Comunicació entre Components", aprendràs a fer que, per exemple, un clic sobre una targeta o un canvi en el seu estat es propaguin realment entre <task-card>, <task-list> i la resta de components de TaskFlow.

Curs de Lit

Mòdul 1: Introducció a Lit i Web Components

Mòdul 2: Plantilles Reactives i Renderitzat

Mòdul 3: Propietats i Estat Reactiu

Mòdul 4: Estils en Components Lit

Mòdul 5: Esdeveniments i Comunicació entre Components

Mòdul 6: Cicle de Vida i Comportament Avançat

Mòdul 7: Directives i Funcionalitats Avançades de Plantilles

Mòdul 8: Integració, Interoperabilitat i Desplegament

Mòdul 9: Proves i Bones Pràctiques

Mòdul 10: Projecte: Construint TaskFlow

© Copyright 2026. Tots els drets reservats