A la lliçó anterior s'ha insistit, amb raó, que el Shadow DOM aixeca una frontera que impedeix que el CSS extern entri en un component Lit. Però aquesta frontera, com es va apuntar de passada a l'apartat 1 de la primera lliçó d'aquest mòdul, té una excepció deliberada: les variables CSS personalitzades. Aquesta lliçó explica per què existeix aquesta excepció, com aprofitar-la per construir components personalitzables des de fora sense trencar la seva encapsulació, i l'aplica a TaskFlow definint variables per als colors de les insígnies d'estat i per a l'avís d'urgència de <task-card>.
Contingut
- L'única esquerda deliberada a la frontera del Shadow DOM
- Sintaxi de les variables CSS: declarar i consumir
- Definir valors per defecte a
:host - Per què les variables CSS són el mecanisme de theming recomanat
- Aplicant-ho a TaskFlow: colors configurables per a estats i urgència
- Personalitzant el tema des de fora del component
- Variables CSS i
:host()amb selector d'atribut
- L'única esquerda deliberada a la frontera del Shadow DOM
La lliçó "CSS Encapsulat amb Shadow DOM" va ser explícita: el CSS del document exterior no entra al shadow root d'un component, i el CSS de dins no surt cap a fora. No obstant això, aquesta afirmació tenia una matisació pendent, assenyalada llavors només de passada: les variables CSS personalitzades (també anomenades custom properties, amb la sintaxi --nombre-variable) sí que travessen aquesta frontera, en totes dues direccions d'herència normal de CSS.
Això no és una particularitat de Lit ni dels Web Components en general: és una decisió de disseny del propi estàndard de les variables CSS, pensada precisament per a aquest escenari. Quan es defineix una variable CSS en qualsevol punt de l'arbre del document —per exemple, a l'element arrel :root, o directament sobre l'etiqueta <task-card> des de fora—, aquesta variable s'hereta de forma normal per tots els seus descendents a l'arbre de renderitzat final, i aquesta herència no s'atura a la frontera d'un Shadow DOM, a diferència de qualsevol altra regla CSS convencional.
/* En el CSS global del documento, fuera de cualquier shadow root */
:root {
--color-primario: #3b82f6;
}// Dentro del shadow root de <task-card>, en su static styles
static styles = css`
h3 {
color: var(--color-primario);
}
`;En aquest exemple, encara que --color-primario es declara completament fora del component, al CSS global de la pàgina, l'h3 de dins del shadow root de <task-card> pot llegir el seu valor amb la funció var() i usar-lo amb total normalitat. És l'única via legítima, per disseny de la plataforma web, de que quelcom definit fora d'un component influeixi en la seva aparença interna sense necessitat de cap propietat JavaScript ni de trencar l'encapsulació d'estils que tant s'ha defensat a les dues lliçons anteriors.
- Sintaxi de les variables CSS: declarar i consumir
Una variable CSS es declara amb dos guions al principi del seu nom, i es llegeix amb la funció var(), que admet un segon argument opcional: un valor per defecte, usat si la variable no està definida en cap punt per sobre a l'arbre d'herència.
selector {
--mi-variable: valor; /* declaración */
}
otro-selector {
propiedad: var(--mi-variable, valor-por-defecto); /* consumo, con valor de respaldo */
}El valor per defecte de var() és, a la pràctica dels Web Components, gairebé tan important com la pròpia variable: és el que permet que un component tingui un aspecte raonable "de fàbrica" fins i tot si ningú, des de fora, defineix mai aquesta variable. Sense un valor per defecte, si la variable no estigués definida en cap lloc, la propietat CSS corresponent simplement no s'aplicaria (es comportaria com si aquella línia de CSS no existís), el que en la majoria dels casos no és el comportament desitjat per a un component pensat per funcionar "llest per usar".
- Definir valors per defecte a
:host
:hostEl patró recomanat per a un component Lit que vol ser personalitzable mitjançant variables CSS és declarar aquestes variables, amb els seus valors per defecte, dins del propi selector :host vist a la primera lliçó d'aquest mòdul. Això no és estrictament obligatori —les variables es poden usar directament amb var() sense declarar-les mai dins del propi component—, però declarar-les a :host documenta, de forma llegible dins del propi CSS del component, quines variables existeixen i quin és el seu valor per defecte, sense haver d'anar a buscar-lo a cada lloc on s'usa var().
Amb aquest plantejament, si ningú des de fora defineix --color-primario, :host l'estableix amb el valor #3b82f6, i h3 l'usa amb normalitat. Però si algú, des del document que conté <task-card>, defineix aquesta mateixa variable amb un valor diferent —per exemple, sobre el propi element <task-card> o sobre qualsevol avantpassat seu a l'arbre del document—, aquesta definició externa té prioritat sobre el valor per defecte declarat a :host, perquè les regles normals d'especificitat i cascada de CSS continuen aplicant-se amb normalitat a les variables, travessant la frontera del Shadow DOM tal com es va explicar a l'apartat 1.
- Per què les variables CSS són el mecanisme de theming recomanat
Amb aquesta base ja es pot entendre per què les variables CSS són, a l'ecosistema de Web Components en general i a Lit en particular, el mecanisme estàndard recomanat per al theming: la possibilitat de que qui usi un component li doni un aspecte visual diferent sense necessitat de modificar el seu codi font.
L'alternativa que podria venir al cap —exposar propietats reactives específiques per a cada aspecte visual, com colorInsigniaHecha o colorAvisoUrgente— té un problema de fons: mesclaria responsabilitats de dades (el que les propietats reactives dels mòduls 2 i 3 han representat fins ara) amb responsabilitats purament d'aparença, i obligaria a declarar una propietat reactiva nova per cada matís visual que es volgués poder personalitzar, amb el cost afegit que cadascuna dispararia innecessàriament el mecanisme complet de reactivitat (i una possible actualització de render()) només per canviar un color.
Les variables CSS, en canvi, resolen el problema a la capa que li correspon —el CSS—, sense tocar en absolut les propietats reactives del component ni provocar cap actualització de render() quan canvien: el navegador es limita a recalcular els estils aplicats, un procés molt més lleuger i que no passa en cap moment pel cicle de vida de Lit descrit al mòdul 2. A més, com qualsevol variable CSS normal, es poden combinar amb media queries, amb classes condicionals al document exterior, o amb qualsevol altre mecanisme estàndard de CSS, sense necessitat que el component sàpiga res de com s'està aplicant el tema des de fora.
- Aplicant-ho a TaskFlow: colors configurables per a estats i urgència
Amb la teoria coberta, és el moment de substituir els colors fixos de <task-card> —fins ara absents o només esbossats a les classes .insignia, .aviso— per variables CSS amb valors per defecte raonables, perquè TaskFlow sigui personalitzable sense tocar el codi font del component.
import { LitElement, html, css } from 'lit';
import { estilosCompartidos } from '../styles/shared-styles.js';
class TaskCard extends LitElement {
static properties = {
// ...igual que en las lecciones anteriores...
};
static styles = [
estilosCompartidos,
css`
:host {
--color-pendiente: #94a3b8;
--color-en-progreso: #f59e0b;
--color-hecha: #22c55e;
--color-urgente: #dc2626;
}
article {
border: 1px solid #d0d5dd;
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.75rem;
background-color: #ffffff;
}
.insignia {
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: 999px;
font-size: 0.8rem;
margin-bottom: 0.5rem;
color: #ffffff;
}
.insignia--pendiente {
background-color: var(--color-pendiente);
}
.insignia--progreso {
background-color: var(--color-en-progreso);
}
.insignia--hecha {
background-color: var(--color-hecha);
}
.aviso {
color: var(--color-urgente);
font-weight: bold;
}
`,
];
// constructor, alternarExpandida, renderInsigniaEstado, renderFechaLimite y render
// se mantienen exactamente igual que en las lecciones anteriores.
}
customElements.define('task-card', TaskCard);Quatre variables noves, declarades totes a :host amb els seus valors per defecte: --color-pendiente, --color-en-progreso, --color-hecha (una per cada estat possible d'una tasca, coherent amb els tres casos ja contemplats a renderInsigniaEstado() des del mòdul 3) i --color-urgente (per a l'avís que apareix quan urgente és true). Cada classe .insignia--* usa var() per llegir la variable corresponent en lloc d'un color fix escrit directament, i el mateix fa .aviso amb --color-urgente. El resultat visual, si ningú defineix cap d'aquestes variables des de fora, és idèntic a tenir aquests mateixos colors escrits directament: els valors per defecte de var() garanteixen que TaskFlow tingui un aspecte raonable "de fàbrica".
- Personalitzant el tema des de fora del component
La raó de ser d'aquest mecanisme s'apreciata en el moment que algú, des del document que usa <task-card>, decideix canviar algun d'aquests colors sense tocar el fitxer task-card.js:
/* En el CSS de la página que usa TaskFlow, completamente fuera de cualquier shadow root */
task-card {
--color-hecha: #15803d;
--color-urgente: #991b1b;
}Amb aquesta regla, qualsevol <task-card> de la pàgina passa a mostrar la seva insígnia de "hecha" i el seu avís d'urgència amb aquests dos tons més foscos, sense que el component hagi canviat ni una línia del seu propi codi: la frontera del Shadow DOM segueix intacta per a la resta de regles CSS (ningú pot, per exemple, canviar el border-radius d'.insignia des d'aquest mateix CSS extern, perquè .insignia no està exposada mitjançant cap variable), però aquestes quatre variables concretes sí que s'han declarat deliberadament com a punts de personalització.
També és perfectament possible declarar les variables en un avantpassat comú, perquè s'apliquin a totes les targetes del tauler alhora sense repetir la regla per cadascuna:
Gràcies a l'herència normal de CSS, declarar aquestes variables a :root (l'element arrel de tot el document) fa que qualsevol <task-card>, estigui on estigui aniuada a la pàgina —fins i tot dins de <task-list>, que al seu torn viu dins d'un altre contenidor—, rebi aquests valors, exactament igual que passaria amb qualsevol propietat CSS heretable en absència de Shadow DOM.
- Variables CSS i
:host() amb selector d'atribut
:host() amb selector d'atributPer tancar aquesta lliçó, convé recuperar breument la menció que es va fer a l'última lliçó del mòdul 3 sobre reflect: true i els selectors d'atribut. Amb la propietat estado reflectida com a atribut (reflect: true, vist en aquella lliçó), és possible combinar :host() —nota la variant amb parèntesis, diferent de :host a seques usada fins ara— amb un selector d'atribut, per aplicar regles diferents segons el valor actual d'aquell atribut, sense necessitat de calcular classes CSS dinàmicament des de render():
Aquesta regla només s'aplica quan el propi element <task-card> té l'atribut estado amb el valor exacte "hecha" —el que, com es va explicar al mòdul 3, requereix que estado estigui declarada amb reflect: true perquè l'atribut es mantingui sincronitzat amb la propietat—, atenuant lleugerament la targeta completa quan la tasca ja està completada. Aquesta tècnica es deixa aquí només apuntada, com a tancament natural del que es va explicar al mòdul anterior: un desenvolupament més complet de condicionals dins de plantilles mitjançant ajudants específics com classMap o styleMap, que ofereixen una forma més còmoda d'alternar classes o estils en línia des de JavaScript, es veurà al mòdul 7, "Directives i Funcionalitats Avançades de Plantilles". Per ara, amb selectors CSS convencionals combinats amb :host() i amb les variables CSS d'aquest apartat, TaskFlow ja disposa d'un sistema de theming complet i funcional.
Errors Comuns i Consells
- Oblidar el valor per defecte a
var()i dependre de que la variable estigui sempre definida: si s'escriucolor: var(--color-hecha);sense segon argument, i cap avantpassat al document defineix mai aquesta variable, la propietatcolorsimplement no s'aplica (no hi ha cap error visible, només l'absència silenciosa de l'estil esperat). Declarar la variable a:host, com s'ha fet a l'apartat 5, evita aquest problema per complet. - Confondre una variable CSS amb una propietat reactiva de Lit:
--color-hechano té cap relació ambstatic propertiesni amb el sistema de reactivitat vist al mòdul 3; és una variable CSS pura, gestionada íntegrament pel navegador mitjançant el mecanisme estàndard de cascada i herència, sense passar en cap moment perrender()ni perrequestUpdate(). - Intentar personalitzar, mitjançant una variable CSS, un aspecte que no s'ha exposat deliberadament com a tal: només les variables que el propi component declara i consumeix amb
var()(com les quatre d'aquesta lliçó) es poden personalitzar des de fora; qualsevol altre valor CSS escrit directament sense passar per una variable (com elborder-radiusd'.insigniaa l'exemple de l'apartat 6) segueix completament encapsulat i no es pot canviar des de fora sense modificar el propi component. - Declarar la variable al selector equivocat del document exterior: si es defineix
--color-hechasobre un element germà de<task-card>, en lloc de sobre<task-card>mateixa o sobre un avantpassat comú de totes dues, l'herència de CSS no la propagarà cap a la targeta, perquè les variables CSS segueixen les mateixes regles d'herència per l'arbre del document que qualsevol altra propietat heretable, no una lògica especial d'"abast global".
Exercicis
- Afegeix a
<task-card>una nova variable--color-borde-tarjeta, amb un valor per defecte de#d0d5dd(el mateix que ja usavaarticlede forma fixa), i substitueix el valor fix delborderd'articlepervar(--color-borde-tarjeta). Comprova, definint aquesta variable amb un valor diferent al CSS d'una pàgina de prova, que la vora de totes les targetes canvia sense tocartask-card.js. - Declara, al CSS global d'una pàgina de prova que contingui diverses
<task-card>, una regla:root { --color-en-progreso: #7c3aed; }, i comprova al navegador que les insígnies de "en progreso" canvien de color a totes les targetes de la pàgina a la vegada, sense haver tocat cap<task-card>de forma individual. - Explica amb les teves pròpies paraules, recolzant-te en l'apartat 4, per què seria una mala idea substituir les quatre variables CSS de colors d'aquesta lliçó per quatre propietats reactives noves (
colorPendiente,colorEnProgreso, etc.) declarades ambstatic properties.
Solucions
static styles = [
estilosCompartidos,
css`
:host {
--color-pendiente: #94a3b8;
--color-en-progreso: #f59e0b;
--color-hecha: #22c55e;
--color-urgente: #dc2626;
--color-borde-tarjeta: #d0d5dd;
}
article {
border: 1px solid var(--color-borde-tarjeta);
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.75rem;
background-color: #ffffff;
}
`,
];Amb task-card { --color-borde-tarjeta: #93c5fd; } al CSS de la pàgina de prova, totes les targetes passen a mostrar una vora blava clara sense cap modificació a task-card.js, exactament pel mateix mecanisme d'herència de variables CSS que travessa el Shadow DOM explicat a l'apartat 1.
-
Com
--color-en-progresoes declara a:root, l'element arrel de tot el document, i les variables CSS s'hereten de forma normal per tots els seus descendents sense que la frontera del Shadow DOM l'aturi (apartat 1), cada<task-card>de la pàgina rep aquest nou valor a través de l'herència, i com.insignia--progresola consumeix ambvar(--color-en-progreso)(apartat 5), totes les insígnies de "en progreso" canvien de color simultàniament, sense necessitat d'aplicar la variable individualment a cada targeta. -
Com s'explica a l'apartat 4, quatre propietats reactives de colors mesclarien una responsabilitat purament visual amb el sistema de propietats reactives pensat per a dades de la tasca (
titulo,estado,prioridad...); cadascuna generaria, a més, un atribut HTML propi i dispararia el cicle complet d'actualització de Lit (incloent-hi una possible execució derender()) cada vegada que canviés, un cost innecessari per a un simple canvi de color. Les variables CSS resolen el mateix problema a la capa que li correspon, el CSS, sense passar en cap moment pel mecanisme de reactivitat de Lit ni perrender(), i a més s'integren de forma natural amb la resta de mecanismes estàndard de CSS (cascada, herència, media queries).
Conclusió
En aquesta lliçó has descobert l'excepció deliberada que les variables CSS suposen davant de l'encapsulació d'estils del Shadow DOM, i has après a declarar-les amb valors per defecte a :host per construir components personalitzables des de fora sense trencar aquesta encapsulació a la resta del seu CSS. Has aplicat aquest mecanisme a <task-card>, definint variables per als colors de cada estat i per a l'avís d'urgència, i has comprovat com personalitzar-les des del document que usa el component, tant de forma individual com a través d'un avantpassat comú com :root.
Amb el CSS encapsulat, els estils compartits i ara el theming mitjançant variables, TaskFlow té ja un sistema d'estils complet i coherent per al contingut que cada component genera des de dins de la seva pròpia plantilla. Però queda un cas diferent per resoldre: què passa quan un component necessita mostrar contingut que no genera ell mateix, sinó que rep de fora, com el nom o la imatge de la persona assignada a una tasca? Aquest és exactament el contingut de la següent lliçó, "Slots i Estilitzat de Contingut Distribuït", on coneixeràs l'element <slot> i crearàs <user-avatar>, el primer component de TaskFlow pensat per rebre contingut distribuït des de fora del seu propi shadow root.
Curs de Lit
Mòdul 1: Introducció a Lit i Web Components
- Què són els Web Components i per què Lit?
- Configuració de l'Entorn de Desenvolupament
- El Teu Primer Component Lit
- Anatomia d'un Component Lit
Mòdul 2: Plantilles Reactives i Renderitzat
- El Motor de Plantilles de Lit
- Expressions i Interpolació en Plantilles
- Renderitzat Condicional
- Renderitzat de Llistes
- El Cicle de Renderitzat
Mòdul 3: Propietats i Estat Reactiu
- Propietats Reactives
- Estat Intern amb @state
- Tipus de Propietats i Conversors Personalitzats
- Atributs vs Propietats i Reflexió
Mòdul 4: Estils en Components Lit
- CSS Encapsulat amb Shadow DOM
- Estils Compartits entre Components
- Variables CSS Personalitzades i Theming
- Slots i Estilitzat de Contingut Distribuït
Mòdul 5: Esdeveniments i Comunicació entre Components
- Gestió d'Esdeveniments DOM en Plantilles
- Esdeveniments Personalitzats: Comunicació de Fill a Pare
- Comunicació de Pare a Fill amb Propietats
- Patrons de Comunicació entre Components Germans
Mòdul 6: Cicle de Vida i Comportament Avançat
- Callbacks del Cicle de Vida
- Hooks Reactius: willUpdate, updated i firstUpdated
- Controladors Reactius
- Mixins i Composició de Comportament
Mòdul 7: Directives i Funcionalitats Avançades de Plantilles
- Directives Incorporades: classMap, styleMap i ifDefined
- Directives Personalitzades
- Renderitzat Asíncron amb until
- Context Compartit amb @lit/context
Mòdul 8: Integració, Interoperabilitat i Desplegament
- Utilitzar Components Lit en HTML Pla
- Integrar Lit amb React, Vue i Angular
- Renderitzat al Servidor amb @lit-labs/ssr
- Empaquetatge, Publicació i TypeScript
Mòdul 9: Proves i Bones Pràctiques
- Proves Unitàries amb Web Test Runner
- Accessibilitat en Web Components
- Rendiment i Optimització
- Patrons i Antipatrons Comuns
