El mòdul 3 ha deixat <task-card> i <task-list> completament resoltes pel que fa a dades: propietats reactives, estat intern, conversors personalitzats i una llista que passa a cada targeta filla les dades de la seva pròpia tasca. Però, com es va assenyalar explícitament en tancar l'última lliçó, tota aquesta feina s'ha fet sobre HTML sense cap estil visual: sense vores, sense tipografia acurada, sense cap color que distingeixi una targeta urgent d'una que no ho és. Aquesta lliçó comença a resoldre aquesta mancança explicant primer un tret fonamental dels Web Components —l'encapsulació d'estils que proporciona el Shadow DOM— i aplicant-lo de seguida per donar a <task-card> la seva primera fulla d'estils pròpia amb static styles i la funció css.

Contingut

  1. Què afegeix el Shadow DOM al CSS: dues fronteres, no una
  2. static styles i la funció css: la manera d'escriure CSS a Lit
  3. Per què no funciona un <link> o un <style> normal dins del shadow root
  4. Alternatives per a CSS realment extern: adoptedStyleSheets
  5. Els primers estils propis de <task-card>
  6. El selector especial :host

  1. Què afegeix el Shadow DOM al CSS: dues fronteres, no una

Abans d'escriure una sola línia de CSS per a <task-card>, convé entendre amb precisió què fa el Shadow DOM amb els estils, perquè és un comportament diferent del de qualsevol element HTML normal i és la base de tot el que es construeix a partir d'aquesta lliçó.

Quan un element no usa Shadow DOM —un <div>, un <article> qualsevol d'una pàgina normal—, el CSS de la pàgina s'aplica sobre ell sense cap barrera: qualsevol regla de qualsevol fulla d'estils carregada al document pot afectar-lo, i qualsevol estil que aquest element defineixi (per exemple, mitjançant una classe) pot afectar altres elements si el selector és prou ampli. És exactament el motiu pel qual, en projectes grans sense Web Components, és habitual acabar amb convencions de nomenclatura elaborades (BEM i similars) només per evitar que el CSS d'una part de l'aplicació "s'escoli" en una altra.

El Shadow DOM canvia això d'arrel, aixecant dues fronteres simultànies:

  • El CSS de fora no entra: les fulles d'estils globals del document, els <link> externs, qualsevol regla p { color: red; } escrita al CSS general de la pàgina, no afecten els elements que viuen dins del shadow root de <task-card>. Per dins, <task-card> comença sempre des d'una fulla en blanc, tret de les propietats CSS heretables per naturalesa (com font-family o color, que sí que travessen la frontera per herència normal de CSS, no per cap característica especial de Lit) i de les variables CSS personalitzades, que es tracten a la següent lliçó d'aquest mòdul.
  • El CSS de dins no surt: qualsevol regla que s'escrigui dins del shadow root de <task-card> —per exemple, article { border: 1px solid; }— només afecta els elements <article> que viuen dins d'aquell shadow root concret. No es filtra cap al document principal ni cap a altres components, ni tan sols cap a altres instàncies de <task-card> amb el seu propi shadow root independent.

Aquesta doble frontera és la raó de fons per la qual els Web Components resolen, de forma nativa i sense cap convenció manual, el problema de col·lisió de noms de classes CSS que arrossega el desenvolupament web des de fa anys. Es pot escriure amb total llibertat una regla com .detalle { padding: 1rem; } dins de <task-card> sense preocupar-se en absolut de si existeix una altra classe .detalle a qualsevol altra part del lloc: viuen en mons d'estils completament separats.

  1. static styles i la funció css: la manera d'escriure CSS a Lit

Lit ofereix un mecanisme dedicat per declarar el CSS d'un component: un camp estàtic anomenat styles, anàleg en esperit a static properties vist al mòdul anterior, el valor del qual es construeix amb una funció especial anomenada css, importada també de lit.

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

class TaskCard extends LitElement {
  static styles = css`
    article {
      border: 1px solid #ccc;
      padding: 1rem;
    }
  `;

  render() {
    return html`<article><h3>Ejemplo</h3></article>`;
  }
}

Fixa't en el paral·lelisme amb html, ja conegut des del mòdul 2: css, igual que html, és una funció que s'usa com a tagged template (una plantilla de JavaScript precedida directament pel nom de la funció, sense parèntesis) i retorna un valor especial que Lit sap interpretar; no és una simple cadena de text, encara que el contingut entre els backticks s'escrigui exactament igual que en un fitxer .css normal. Aquesta semblança de sintaxi entre html i css no és casualitat: totes dues formen part del mateix sistema de plantilles etiquetades de Lit, pensat perquè l'editor de codi pugui oferir ressaltat de sintaxi (amb les extensions adequades) tant per al marcatge com per als estils, directament dins del fitxer JavaScript.

static styles es declara, igual que static properties, una única vegada, fora de qualsevol mètode, com a camp estàtic de la classe. Lit el processa quan es defineix el component i, en temps d'execució, insereix aquest CSS dins del propi shadow root de cada instància, de manera que les dues fronteres descrites a l'apartat anterior s'apliquin automàticament sense cap treball addicional per la teva part.

  1. Per què no funciona un <link> o un <style> normal dins del shadow root

És raonable preguntar-se, sobretot si es ve d'escriure HTML i CSS de forma tradicional, per què no n'hi ha prou amb incloure una etiqueta <style> (o un <link rel="stylesheet"> apuntant a un fitxer .css) directament dins de la plantilla render(), en lloc d'usar static styles.

// Funciona, però amb un problema de rendiment important
render() {
  return html`
    <style>
      article { border: 1px solid #ccc; padding: 1rem; }
    </style>
    <article><h3>Ejemplo</h3></article>
  `;
}

Tècnicament, una etiqueta <style> escrita així dins de render() sí que queda encapsulada pel Shadow DOM de la mateixa manera que la resta del contingut: les seves regles només afectaran aquest shadow root. El problema no és d'encapsulació, sinó de rendiment: com es va explicar a la lliçó del cicle de renderitzat del mòdul 2, render() es torna a executar cada vegada que canvia una propietat reactiva, i amb aquest plantejament l'etiqueta <style> completa —i, en molts navegadors, el CSS que conté— es recrearia i es tornaria a processar en cadascuna d'aquestes actualitzacions, un cost completament innecessari per a un CSS que gairebé sempre és el mateix en cada renderitzat.

static styles, en canvi, es processa una sola vegada per definició de classe (no per instància, ni per renderitzat): Lit construeix internament una representació optimitzada del CSS i la reutilitza en totes les actualitzacions i, fins i tot, en totes les instàncies del mateix component que coexisteixin a la pàgina. És, de llarg, la manera recomanada de declarar estils a Lit, i l'única que s'usa a la resta d'aquest curs.

Pel que fa a un <link rel="stylesheet" href="..."> normal apuntant a un fitxer .css extern, col·locat dins del render(): a la pràctica funciona en navegadors moderns, però té un problema afegit de temps: el shadow root es renderitza abans que el navegador acabi de descarregar la fulla d'estils externa, així que es produeix gairebé sempre un parpelleig visible de contingut sense estil (el fenomen conegut com FOUC, flash of unstyled content) durant el primer renderitzat, cada vegada que es crea una nova instància del component. Per aquest motiu, i per l'avantatge de rendiment ja explicat, static styles amb css és la via recomanada per la mateixa documentació de Lit per al cas normal d'un component amb el seu CSS conegut de bestreta.

  1. Alternatives per a CSS realment extern: adoptedStyleSheets

Existeix un escenari legítim en el qual sí que interessa carregar CSS des d'una font veritablement externa al propi mòdul del component: per exemple, una fulla d'estils generada dinàmicament, o compartida mitjançant una API de baix nivell del navegador anomenada adoptedStyleSheets, que permet construir un objecte CSSStyleSheet mitjançant JavaScript i "adoptar-lo" en un o diversos shadow roots sense necessitat que cadascun tingui la seva pròpia còpia del CSS en memòria.

Lit usa precisament adoptedStyleSheets per sota, de forma transparent, quan el navegador ho suporta (amb una alternativa automàtica per als pocs navegadors que no ho fan), com a mecanisme d'implementació de static styles. No cal, en l'ús normal de Lit, tocar adoptedStyleSheets directament: s'esmenta aquí únicament perquè quedi clar que static styles no és una limitació del framework, sinó una capa còmoda construïda sobre una API estàndard de la plataforma web, pensada exactament per a aquest propòsit de compartir CSS entre shadow roots de forma eficient. La següent lliçó d'aquest mòdul, "Estils Compartits entre Components", torna a aquesta idea de compartir CSS, però treballant sempre al nivell de static styles i css, que és la via pràctica del dia a dia amb Lit.

  1. Els primers estils propis de <task-card>

Amb la teoria ja coberta, és el moment de donar a <task-card> la seva primera aparença visual real. Recupera el fitxer src/components/task-card.js tal com va quedar al final del mòdul 3 —amb les seves propietats reactives, el seu estat intern expandida i la seva insígnia d'estat— i afegeix static styles:

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

const conversorDeFecha = {
  fromAttribute(valorDelAtributo) {
    if (!valorDelAtributo) {
      return null;
    }
    return new Date(valorDelAtributo);
  },
  toAttribute(valorDeLaPropiedad) {
    if (!valorDeLaPropiedad) {
      return null;
    }
    return valorDeLaPropiedad.toISOString().split('T')[0];
  },
};

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

  static styles = css`
    article {
      border: 1px solid #d0d5dd;
      border-radius: 8px;
      padding: 1rem;
      margin-bottom: 0.75rem;
      font-family: system-ui, sans-serif;
      background-color: #ffffff;
    }

    h3 {
      margin: 0 0 0.5rem 0;
      font-size: 1.1rem;
      color: #1f2933;
    }

    p {
      margin: 0.25rem 0;
      font-size: 0.9rem;
      color: #52606d;
    }

    .insignia {
      display: inline-block;
      padding: 0.15rem 0.5rem;
      border-radius: 999px;
      font-size: 0.8rem;
      margin-bottom: 0.5rem;
    }

    .aviso {
      color: #b42318;
      font-weight: bold;
    }
  `;

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

  alternarExpandida() {
    this.expandida = !this.expandida;
  }

  renderInsigniaEstado() {
    if (this.estado === 'hecha') {
      return html`<span class="insignia insignia--hecha">✓ Hecha</span>`;
    }
    if (this.estado === 'en-progreso') {
      return html`<span class="insignia insignia--progreso">◐ En progreso</span>`;
    }
    return html`<span class="insignia insignia--pendiente">○ Pendiente</span>`;
  }

  renderFechaLimite() {
    if (!this.fechaLimite) {
      return '';
    }
    return html`<p>Fecha límite: ${this.fechaLimite.toLocaleDateString('es-ES')}</p>`;
  }

  render() {
    return html`
      <article @click="${this.alternarExpandida}">
        <h3>${this.titulo}</h3>
        ${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);

El bloc static styles conté regles CSS completament normals —selectors d'etiqueta (article, h3, p) i de classe (.insignia, .aviso)—, exactament igual que en qualsevol fulla d'estils tradicional. L'únic diferent és on viu aquest CSS i a què afecta: com es va explicar a l'apartat 1, la regla article { border: 1px solid #d0d5dd; ... } només pot afectar l'<article> que la pròpia plantilla de <task-card> genera, sense risc de xocar amb cap altre <article> que pogués existir a qualsevol altra part de TaskFlow (per exemple, dins de <task-list>, que a la següent lliçó d'aquest mòdul també usarà el seu propi element d'estructura). Nota, a més, que les classes .insignia--hecha, .insignia--progreso i .insignia--pendiente, encara que generades per renderInsigniaEstado(), no tenen encara cap regla pròpia en aquest primer pas: es recuperen amb els seus colors concrets a la lliçó "Variables CSS Personalitzades i Theming" d'aquest mateix mòdul.

  1. El selector especial :host

Abans de tancar aquesta primera lliçó, convé presentar un selector que no existeix en el CSS tradicional i que resultarà imprescindible a la resta del mòdul: :host. Dins d'una fulla d'estils declarada amb static styles, :host selecciona el propi element personalitzat, és a dir, l'etiqueta <task-card> tal com es veu des de fora del shadow root, no cap element de dins de la plantilla.

static styles = css`
  :host {
    display: block;
  }

  article {
    border: 1px solid #d0d5dd;
    /* ... */
  }
`;

Aquesta regla :host { display: block; } és, de fet, una pràctica habitual i recomanada en començar a donar estil a qualsevol component Lit: per defecte, un element personalitzat sense cap CSS associat es comporta com un element inline (igual que un <span>), el que pot produir comportaments de maquetació poc intuïtius si s'espera que ocupi tot l'ample disponible o que respecti correctament els marges; declarar :host { display: block; } fa que <task-card> es comporti, de cara al document que el conté, com un bloc normal. :host és també la peça clau per poder escriure selectors com :host([estado="hecha"]), esmentats a l'última lliçó del mòdul 3 a propòsit de reflect: true, encara que aquest ús concret —amb variables CSS i selecció per atribut— es desenvolupa amb més detall a les lliçons següents d'aquest mòdul.

Errors Comuns i Consells

  • Esperar que una classe CSS global de la pàgina afecti un component Lit: com es va explicar a l'apartat 1, el CSS extern al shadow root no hi entra sota cap circumstància (llevat d'herència de propietats heretables i variables CSS, que es veuen a la següent lliçó). Si una regla escrita en una fulla d'estils general del lloc no sembla aplicar-se a <task-card>, la causa gairebé sempre és aquesta frontera, no un error de sintaxi.
  • Escriure el CSS amb una etiqueta <style> dins de render() "perquè funciona": com es va detallar a l'apartat 3, aquesta tècnica encapsula correctament el CSS, però el reprocessa en cada actualització del component, un cost innecessari que static styles evita per disseny. Excepte casos molt concrets de CSS generat dinàmicament, static styles és sempre l'opció a preferir.
  • Oblidar :host { display: block; } i sorprendre's pel comportament de maquetació del component: un element personalitzat sense estil propi es comporta com inline per defecte; si <task-card> sembla no respectar l'ample o els marges esperats dins de <task-list>, convé revisar primer si s'ha declarat un display explícit a :host.
  • Intentar usar un <link rel="stylesheet"> dins del shadow root esperant que carregui de forma instantània: com es va apuntar a l'apartat 3, un <link> extern introdueix una espera de xarxa que pot provocar un parpelleig de contingut sense estil a cada nova instància del component; per al CSS propi d'un component, conegut de bestreta, static styles no té aquest problema perquè el CSS ja viatja inclòs en el propi mòdul JavaScript.

Exercicis

  1. Afegeix a la fulla d'estils de <task-card> una regla :host { display: block; } i comprova, col·locant dues o més <task-card> seguides en una pàgina de prova sense cap altre CSS extern, que cadascuna ocupa la seva pròpia línia en lloc d'aparèixer una al costat de l'altra.
  2. Escriu, en una pàgina HTML de prova fora del shadow root de <task-card>, una regla article { border: 5px solid red; } en una fulla d'estils normal del document, i comprova al navegador que la vora vermella no afecta en absolut l'<article> intern de <task-card>. Explica amb les teves pròpies paraules, recolzant-te en l'apartat 1, per què passa això.
  3. Afegeix una nova regla a static styles que doni a la classe .detalle (el bloc que apareix quan expandida és true) un fons gris clar i un padding propi, i comprova que aquest estil només s'aplica quan la targeta està expandida, sense haver de tocar res de la lògica de render().

Solucions

static styles = css`
  :host {
    display: block;
  }

  article {
    border: 1px solid #d0d5dd;
    border-radius: 8px;
    padding: 1rem;
    margin-bottom: 0.75rem;
  }
`;

Sense :host { display: block; }, dues <task-card> seguides a l'HTML tendirien a col·locar-se a la mateixa línia, com passa amb qualsevol element inline (per exemple, dos <span> consecutius); amb la regla afegida, cada <task-card> passa a comportar-se com un bloc independent i les targetes s'apilen verticalment, una per línia, tal com caldria esperar d'una llista de tasques.

  1. La vora vermella no apareix a l'<article> de dins de <task-card> perquè, com s'explica a l'apartat 1, el Shadow DOM aixeca una frontera que impedeix que el CSS del document exterior entri al shadow root del component. La regla article { border: 5px solid red; }, en estar definida fora d'aquest shadow root, només podria afectar elements <article> que visquin igualment fora de qualsevol shadow root (o dins de shadow roots que hagin declarat explícitament aquesta mateixa regla), mai a l'<article> intern de <task-card>, que només obeeix al CSS declarat en el seu propi static styles.

static styles = css`
  /* ...regles anteriors... */

  .detalle {
    background-color: #f2f4f7;
    padding: 0.5rem;
    border-radius: 4px;
    margin-top: 0.5rem;
  }
`;

Com .detalle només apareix a l'HTML generat per render() quan this.expandida és true (segons la lògica ja existent del mòdul 3), la regla d'estil s'aplica automàticament només en aquest moment, sense cap necessitat de tocar la condició del render(): el CSS descriu com es veu un element quan existeix, i és la pròpia plantilla la que decideix quan aquell element existeix.

Conclusió

En aquesta lliçó has entès la doble frontera que aixeca el Shadow DOM entre el CSS d'un component i la resta del document, has après a declarar estils amb static styles i la funció css, i has vist per què aquesta via és preferible a una etiqueta <style> dins de render() o a un <link> extern dins del shadow root. Amb tot això, <task-card> té ja la seva primera aparença visual pròpia: una targeta amb vora, tipografia acurada i un :host correctament configurat com a bloc.

No obstant això, en el moment que es doni estil també a <task-list> (que necessitarà el seu propi contenidor, el seu propi títol, potser la seva pròpia tipografia base), apareixerà de seguida un problema pràctic: molt d'aquest CSS —colors base, tipografia, espaiats— té sentit compartir-lo entre diversos components, no repetir-lo copiat i enganxat a cadascun. Aquest és exactament el contingut de la següent lliçó, "Estils Compartits entre Components", on aprendràs a extraure una fulla d'estils comuna reutilitzable entre <task-card> i <task-list>.

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