En la lección anterior se ha insistido, con razón, en que el Shadow DOM levanta una frontera que impide que el CSS externo entre en un componente Lit. Pero esa frontera, como se apuntó de pasada en el apartado 1 de la primera lección de este módulo, tiene una excepción deliberada: las variables CSS personalizadas. Esta lección explica por qué esa excepción existe, cómo aprovecharla para construir componentes personalizables desde fuera sin romper su encapsulación, y la aplica a TaskFlow definiendo variables para los colores de las insignias de estado y para el aviso de urgencia de <task-card>.

Contenido

  1. La única grieta deliberada en la frontera del Shadow DOM
  2. Sintaxis de las variables CSS: declarar y consumir
  3. Definir valores por defecto en :host
  4. Por qué las variables CSS son el mecanismo de theming recomendado
  5. Aplicándolo a TaskFlow: colores configurables para estados y urgencia
  6. Personalizando el tema desde fuera del componente
  7. Variables CSS y :host() con selector de atributo

  1. La única grieta deliberada en la frontera del Shadow DOM

La lección "CSS Encapsulado con Shadow DOM" fue explícita: el CSS del documento exterior no entra en el shadow root de un componente, y el CSS de dentro no sale hacia fuera. Sin embargo, esa afirmación tenía una matización pendiente, señalada entonces solo de pasada: las variables CSS personalizadas (también llamadas custom properties, con la sintaxis --nombre-variable) sí atraviesan esa frontera, en ambas direcciones de herencia normal de CSS.

Esto no es una particularidad de Lit ni de los Web Components en general: es una decisión de diseño del propio estándar de las variables CSS, pensada precisamente para este escenario. Cuando se define una variable CSS en cualquier punto del árbol del documento —por ejemplo, en el elemento raíz :root, o directamente sobre la etiqueta <task-card> desde fuera—, esa variable se hereda de forma normal por todos sus descendientes en el árbol de renderizado final, y esa herencia no se detiene en la frontera de un Shadow DOM, a diferencia de cualquier otra 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 este ejemplo, aunque --color-primario se declara completamente fuera del componente, en el CSS global de la página, el h3 de dentro del shadow root de <task-card> puede leer su valor con la función var() y usarlo con total normalidad. Es la única vía legítima, por diseño de la plataforma web, de que algo definido fuera de un componente influya en su apariencia interna sin necesidad de ninguna propiedad JavaScript ni de romper la encapsulación de estilos que tanto se ha defendido en las dos lecciones anteriores.

  1. Sintaxis de las variables CSS: declarar y consumir

Una variable CSS se declara con dos guiones al principio de su nombre, y se lee con la función var(), que admite un segundo argumento opcional: un valor por defecto, usado si la variable no está definida en ningún punto por encima en el árbol de herencia.

selector {
  --mi-variable: valor;       /* declaración */
}

otro-selector {
  propiedad: var(--mi-variable, valor-por-defecto);   /* consumo, con valor de respaldo */
}

El valor por defecto de var() es, en la práctica de los Web Components, casi tan importante como la propia variable: es lo que permite que un componente tenga un aspecto razonable "de fábrica" incluso si nadie, desde fuera, define nunca esa variable. Sin un valor por defecto, si la variable no estuviera definida en ningún sitio, la propiedad CSS correspondiente simplemente no se aplicaría (se comportaría como si esa línea de CSS no existiera), lo que en la mayoría de los casos no es el comportamiento deseado para un componente pensado para funcionar "listo para usar".

  1. Definir valores por defecto en :host

El patrón recomendado para un componente Lit que quiere ser personalizable mediante variables CSS es declarar esas variables, con sus valores por defecto, dentro del propio selector :host visto en la primera lección de este módulo. Esto no es estrictamente obligatorio —las variables se pueden usar directamente con var() sin declararlas nunca dentro del propio componente—, pero declararlas en :host documenta, de forma legible dentro del propio CSS del componente, qué variables existen y cuál es su valor por defecto, sin tener que ir a buscarlo a cada sitio donde se usa var().

static styles = css`
  :host {
    --color-primario: #3b82f6;
  }

  h3 {
    color: var(--color-primario);
  }
`;

Con este planteamiento, si nadie desde fuera define --color-primario, :host la establece con el valor #3b82f6, y h3 la usa con normalidad. Pero si alguien, desde el documento que contiene a <task-card>, define esa misma variable con un valor distinto —por ejemplo, sobre el propio elemento <task-card> o sobre cualquier antepasado suyo en el árbol del documento—, esa definición externa tiene prioridad sobre el valor por defecto declarado en :host, porque las reglas normales de especificidad y cascada de CSS siguen aplicándose con normalidad a las variables, atravesando la frontera del Shadow DOM tal como se explicó en el apartado 1.

  1. Por qué las variables CSS son el mecanismo de theming recomendado

Con esta base ya se puede entender por qué las variables CSS son, en el ecosistema de Web Components en general y en Lit en particular, el mecanismo estándar recomendado para el theming: la posibilidad de que quien use un componente le dé un aspecto visual distinto sin necesidad de modificar su código fuente.

La alternativa que podría venir a la mente —exponer propiedades reactivas específicas para cada aspecto visual, como colorInsigniaHecha o colorAvisoUrgente— tiene un problema de fondo: mezclaría responsabilidades de datos (lo que las propiedades reactivas de los módulos 2 y 3 han representado hasta ahora) con responsabilidades puramente de apariencia, y obligaría a declarar una propiedad reactiva nueva por cada matiz visual que se quisiera poder personalizar, con el coste añadido de que cada una dispararía innecesariamente el mecanismo completo de reactividad (y una posible actualización de render()) solo por cambiar un color.

Las variables CSS, en cambio, resuelven el problema en la capa que le corresponde —el CSS—, sin tocar en absoluto las propiedades reactivas del componente ni provocar ninguna actualización de render() cuando cambian: el navegador se limita a recalcular los estilos aplicados, un proceso mucho más ligero y que no pasa en ningún momento por el ciclo de vida de Lit descrito en el módulo 2. Además, como cualquier variable CSS normal, se pueden combinar con media queries, con clases condicionales en el documento exterior, o con cualquier otro mecanismo estándar de CSS, sin necesidad de que el componente sepa nada de cómo se está aplicando el tema desde fuera.

  1. Aplicándolo a TaskFlow: colores configurables para estados y urgencia

Con la teoría cubierta, es el momento de sustituir los colores fijos de <task-card> —hasta ahora ausentes o solo esbozados en las clases .insignia, .aviso— por variables CSS con valores por defecto razonables, para que TaskFlow sea personalizable sin tocar el código fuente del componente.

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);

Cuatro variables nuevas, declaradas todas en :host con sus valores por defecto: --color-pendiente, --color-en-progreso, --color-hecha (una por cada estado posible de una tarea, coherente con los tres casos ya contemplados en renderInsigniaEstado() desde el módulo 3) y --color-urgente (para el aviso que aparece cuando urgente es true). Cada clase .insignia--* usa var() para leer la variable correspondiente en lugar de un color fijo escrito directamente, y lo mismo hace .aviso con --color-urgente. El resultado visual, si nadie define ninguna de estas variables desde fuera, es idéntico a tener esos mismos colores escritos directamente: los valores por defecto de var() garantizan que TaskFlow tenga un aspecto razonable "de fábrica".

  1. Personalizando el tema desde fuera del componente

La razón de ser de este mecanismo se aprecia en cuanto alguien, desde el documento que usa <task-card>, decide cambiar alguno de esos colores sin tocar el fichero 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;
}

Con esta regla, cualquier <task-card> de la página pasa a mostrar su insignia de "hecha" y su aviso de urgencia con estos dos tonos más oscuros, sin que el componente haya cambiado ni una línea de su propio código: la frontera del Shadow DOM sigue intacta para el resto de reglas CSS (nadie puede, por ejemplo, cambiar el border-radius de .insignia desde este mismo CSS externo, porque .insignia no está expuesta mediante ninguna variable), pero estas cuatro variables concretas sí se han declarado deliberadamente como puntos de personalización.

También es perfectamente posible declarar las variables en un antepasado común, para que se apliquen a todas las tarjetas del tablero a la vez sin repetir la regla por cada una:

:root {
  --color-hecha: #15803d;
  --color-urgente: #991b1b;
}

Gracias a la herencia normal de CSS, declarar estas variables en :root (el elemento raíz de todo el documento) hace que cualquier <task-card>, esté donde esté anidada en la página —incluso dentro de <task-list>, que a su vez vive dentro de otro contenedor—, reciba esos valores, exactamente igual que ocurriría con cualquier propiedad CSS heredable en ausencia de Shadow DOM.

  1. Variables CSS y :host() con selector de atributo

Para cerrar esta lección, conviene retomar brevemente la mención que se hizo en la última lección del módulo 3 sobre reflect: true y los selectores de atributo. Con la propiedad estado reflejada como atributo (reflect: true, visto en esa lección), es posible combinar :host() —nótese la variante con paréntesis, distinta de :host a secas usada hasta ahora— con un selector de atributo, para aplicar reglas distintas según el valor actual de ese atributo, sin necesidad de calcular clases CSS dinámicamente desde render():

static styles = css`
  :host([estado="hecha"]) article {
    opacity: 0.7;
  }
`;

Esta regla solo se aplica cuando el propio elemento <task-card> tiene el atributo estado con el valor exacto "hecha" —lo cual, como se explicó en el módulo 3, requiere que estado esté declarada con reflect: true para que el atributo se mantenga sincronizado con la propiedad—, atenuando ligeramente la tarjeta completa cuando la tarea ya está completada. Esta técnica se deja aquí solo apuntada, como cierre natural de lo que se explicó en el módulo anterior: un desarrollo más completo de condicionales dentro de plantillas mediante ayudantes específicos como classMap o styleMap, que ofrecen una forma más cómoda de alternar clases o estilos en línea desde JavaScript, se verá en el módulo 7, "Directivas y Funcionalidades Avanzadas de Plantillas". Por ahora, con selectores CSS convencionales combinados con :host() y con las variables CSS de este apartado, TaskFlow ya dispone de un sistema de theming completo y funcional.

Errores Comunes y Consejos

  • Olvidar el valor por defecto en var() y depender de que la variable siempre esté definida: si se escribe color: var(--color-hecha); sin segundo argumento, y ningún antepasado en el documento define nunca esa variable, la propiedad color sencillamente no se aplica (no hay ningún error visible, solo la ausencia silenciosa del estilo esperado). Declarar la variable en :host, como se ha hecho en el apartado 5, evita este problema por completo.
  • Confundir una variable CSS con una propiedad reactiva de Lit: --color-hecha no tiene ninguna relación con static properties ni con el sistema de reactividad visto en el módulo 3; es una variable CSS pura, gestionada enteramente por el navegador mediante el mecanismo estándar de cascada y herencia, sin pasar en ningún momento por render() ni por requestUpdate().
  • Intentar personalizar, mediante una variable CSS, un aspecto que no se ha expuesto deliberadamente como tal: solo las variables que el propio componente declara y consume con var() (como las cuatro de esta lección) se pueden personalizar desde fuera; cualquier otro valor CSS escrito directamente sin pasar por una variable (como el border-radius de .insignia en el ejemplo del apartado 6) sigue completamente encapsulado y no se puede cambiar desde fuera sin modificar el propio componente.
  • Declarar la variable en el selector equivocado del documento exterior: si se define --color-hecha sobre un elemento hermano de <task-card>, en lugar de sobre <task-card> misma o sobre un antepasado común de ambos, la herencia de CSS no la propagará hacia la tarjeta, porque las variables CSS siguen las mismas reglas de herencia por el árbol del documento que cualquier otra propiedad heredable, no una lógica especial de "alcance global".

Ejercicios

  1. Añade a <task-card> una nueva variable --color-borde-tarjeta, con un valor por defecto de #d0d5dd (el mismo que ya usaba article de forma fija), y sustituye el valor fijo del border de article por var(--color-borde-tarjeta). Comprueba, definiendo esa variable con un valor distinto en el CSS de una página de prueba, que el borde de todas las tarjetas cambia sin tocar task-card.js.
  2. Declara, en el CSS global de una página de prueba que contenga varias <task-card>, una regla :root { --color-en-progreso: #7c3aed; }, y comprueba en el navegador que las insignias de "en progreso" cambian de color en todas las tarjetas de la página a la vez, sin haber tocado ninguna <task-card> de forma individual.
  3. Explica con tus propias palabras, apoyándote en el apartado 4, por qué sería una mala idea sustituir las cuatro variables CSS de colores de esta lección por cuatro propiedades reactivas nuevas (colorPendiente, colorEnProgreso, etc.) declaradas con static properties.

Soluciones

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;
    }
  `,
];

Con task-card { --color-borde-tarjeta: #93c5fd; } en el CSS de la página de prueba, todas las tarjetas pasan a mostrar un borde azul claro sin ninguna modificación en task-card.js, exactamente por el mismo mecanismo de herencia de variables CSS que atraviesa el Shadow DOM explicado en el apartado 1.

  1. Como --color-en-progreso se declara en :root, el elemento raíz de todo el documento, y las variables CSS se heredan de forma normal por todos sus descendientes sin que la frontera del Shadow DOM la detenga (apartado 1), cada <task-card> de la página recibe ese nuevo valor a través de la herencia, y como .insignia--progreso la consume con var(--color-en-progreso) (apartado 5), todas las insignias de "en progreso" cambian de color simultáneamente, sin necesidad de aplicar la variable individualmente a cada tarjeta.

  2. Como se explica en el apartado 4, cuatro propiedades reactivas de colores mezclarían una responsabilidad puramente visual con el sistema de propiedades reactivas pensado para datos de la tarea (titulo, estado, prioridad...); cada una generaría, además, un atributo HTML propio y dispararía el ciclo completo de actualización de Lit (incluyendo una posible ejecución de render()) cada vez que cambiara, un coste innecesario para un simple cambio de color. Las variables CSS resuelven el mismo problema en la capa que le corresponde, el CSS, sin pasar en ningún momento por el mecanismo de reactividad de Lit ni por render(), y además se integran de forma natural con el resto de mecanismos estándar de CSS (cascada, herencia, media queries).

Conclusión

En esta lección has descubierto la excepción deliberada que las variables CSS suponen frente a la encapsulación de estilos del Shadow DOM, y has aprendido a declararlas con valores por defecto en :host para construir componentes personalizables desde fuera sin romper esa encapsulación en el resto de su CSS. Has aplicado este mecanismo a <task-card>, definiendo variables para los colores de cada estado y para el aviso de urgencia, y has comprobado cómo personalizarlas desde el documento que usa el componente, tanto de forma individual como a través de un antepasado común como :root.

Con el CSS encapsulado, los estilos compartidos y ahora el theming mediante variables, TaskFlow tiene ya un sistema de estilos completo y coherente para el contenido que cada componente genera desde dentro de su propia plantilla. Pero queda un caso distinto por resolver: ¿qué ocurre cuando un componente necesita mostrar contenido que no genera él mismo, sino que recibe de fuera, como el nombre o la imagen de la persona asignada a una tarea? Ese es exactamente el contenido de la siguiente lección, "Slots y Estilizado de Contenido Distribuido", donde conocerás el elemento <slot> y crearás <user-avatar>, el primer componente de TaskFlow pensado para recibir contenido distribuido desde fuera de su propio shadow root.

Curso de Lit

Módulo 1: Introducción a Lit y Web Components

Módulo 2: Plantillas Reactivas y Renderizado

Módulo 3: Propiedades y Estado Reactivo

Módulo 4: Estilos en Componentes Lit

Módulo 5: Eventos y Comunicación entre Componentes

Módulo 6: Ciclo de Vida y Comportamiento Avanzado

Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas

Módulo 8: Integración, Interoperabilidad y Despliegue

Módulo 9: Pruebas y Buenas Prácticas

Módulo 10: Proyecto: Construyendo TaskFlow

© Copyright 2026. Todos los derechos reservados