The previous lesson rightly insisted that Shadow DOM raises a boundary that keeps external CSS from getting into a Lit component. But that boundary, as noted in passing in section 1 of the first lesson of this module, has one deliberate exception: custom CSS properties. This lesson explains why that exception exists, how to take advantage of it to build components that are customizable from outside without breaking their encapsulation, and applies it to TaskFlow by defining variables for the status badge colors and for <task-card>'s urgency warning.
Contents
- The one deliberate crack in the Shadow DOM boundary
- CSS variable syntax: declaring and consuming
- Defining default values on
:host - Why CSS variables are the recommended theming mechanism
- Applying it to TaskFlow: configurable colors for statuses and urgency
- Customizing the theme from outside the component
- CSS variables and
:host()with an attribute selector
- The one deliberate crack in the Shadow DOM boundary
The "Encapsulated CSS with Shadow DOM" lesson was explicit: CSS from the outer document doesn't get into a component's shadow root, and CSS from inside doesn't get out. However, that statement had a pending caveat, noted then only in passing: custom CSS properties (also called custom properties, with the --variable-name syntax) do cross that boundary, in both directions of normal CSS inheritance.
This isn't a peculiarity of Lit or of Web Components in general: it's a design decision of the CSS custom properties standard itself, designed precisely for this scenario. When a CSS variable is defined at any point in the document tree — for example, on the root :root element, or directly on the <task-card> tag from the outside — that variable is inherited normally by all of its descendants in the final render tree, and that inheritance does not stop at a Shadow DOM boundary, unlike any other conventional CSS rule.
/* 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);
}
`;In this example, even though --color-primario is declared entirely outside the component, in the page's global CSS, the h3 inside <task-card>'s shadow root can read its value with the var() function and use it completely normally. It's the only legitimate way, by web-platform design, for something defined outside a component to influence its internal appearance without needing any JavaScript property and without breaking the style encapsulation defended so strongly in the previous two lessons.
- CSS variable syntax: declaring and consuming
A CSS variable is declared with two dashes at the start of its name, and read with the var() function, which accepts an optional second argument: a default value, used if the variable isn't defined anywhere further up the inheritance tree.
selector {
--mi-variable: valor; /* declaración */
}
otro-selector {
propiedad: var(--mi-variable, valor-por-defecto); /* consumo, con valor de respaldo */
}The default value in var() is, in Web Components practice, almost as important as the variable itself: it's what lets a component have a reasonable "out of the box" look even if nobody, from outside, ever defines that variable. Without a default value, if the variable weren't defined anywhere, the corresponding CSS property simply wouldn't apply (it would behave as if that CSS line didn't exist), which in most cases isn't the desired behavior for a component meant to work "ready to use."
- Defining default values on
:host
:hostThe recommended pattern for a Lit component that wants to be customizable through CSS variables is to declare those variables, with their default values, inside the :host selector seen in the first lesson of this module. This isn't strictly required — variables can be used directly with var() without ever declaring them inside the component itself — but declaring them on :host documents, in a readable way inside the component's own CSS, which variables exist and what their default value is, without having to hunt it down at every place where var() is used.
With this approach, if nobody from outside defines --color-primario, :host sets it to #3b82f6, and h3 uses it normally. But if someone, from the document containing <task-card>, defines that same variable with a different value — for example, on the <task-card> element itself or on any of its ancestors in the document tree — that external definition takes priority over the default value declared on :host, because the normal rules of CSS specificity and cascade still apply normally to variables, crossing the Shadow DOM boundary just as explained in section 1.
- Why CSS variables are the recommended theming mechanism
With this foundation, it's now possible to understand why CSS variables are, in the Web Components ecosystem in general and in Lit in particular, the standard recommended mechanism for theming: the ability for whoever uses a component to give it a different visual appearance without needing to modify its source code.
The alternative that might come to mind — exposing specific reactive properties for each visual aspect, such as colorInsigniaHecha or colorAvisoUrgente — has an underlying problem: it would mix data responsibilities (what reactive properties from modules 2 and 3 have represented so far) with purely appearance-related responsibilities, and it would require declaring a new reactive property for every visual nuance one wanted to customize, with the added cost that each one would unnecessarily trigger the full reactivity mechanism (and a possible render() update) just to change a color.
CSS variables, on the other hand, solve the problem at the layer where it belongs — CSS — without touching the component's reactive properties at all and without triggering any render() update when they change: the browser simply recalculates the applied styles, a much lighter process that never goes through the Lit lifecycle described in module 2. In addition, like any normal CSS variable, they can be combined with media queries, with conditional classes in the outer document, or with any other standard CSS mechanism, without the component needing to know anything about how the theme is being applied from outside.
- Applying it to TaskFlow: configurable colors for statuses and urgency
With the theory covered, it's time to replace <task-card>'s fixed colors — so far absent or only sketched out in the .insignia and .aviso classes — with CSS variables that have reasonable default values, so that TaskFlow becomes customizable without touching the component's source code.
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);Four new variables, all declared on :host with their default values: --color-pendiente, --color-en-progreso, --color-hecha (one for each possible task status, consistent with the three cases already handled in renderInsigniaEstado() since module 3) and --color-urgente (for the warning that appears when urgente is true). Each .insignia--* class uses var() to read the corresponding variable instead of a fixed color written directly, and .aviso does the same with --color-urgente. The visual result, if nobody defines any of these variables from outside, is identical to having those same colors written directly: var()'s default values guarantee that TaskFlow has a reasonable "out of the box" look.
- Customizing the theme from outside the component
The whole point of this mechanism becomes clear as soon as someone, from the document that uses <task-card>, decides to change one of these colors without touching the task-card.js file:
/* En el CSS de la página que usa TaskFlow, completamente fuera de cualquier shadow root */
task-card {
--color-hecha: #15803d;
--color-urgente: #991b1b;
}With this rule, any <task-card> on the page starts showing its "hecha" badge and its urgency warning in these two darker shades, without the component having changed a single line of its own code: the Shadow DOM boundary remains intact for the rest of the CSS rules (nobody can, for example, change the border-radius of .insignia from this same external CSS, because .insignia isn't exposed through any variable), but these four specific variables have been deliberately declared as customization points.
It's also perfectly possible to declare the variables on a common ancestor, so they apply to every card on the board at once without repeating the rule for each one:
Thanks to normal CSS inheritance, declaring these variables on :root (the document's root element) makes any <task-card>, wherever it's nested on the page — even inside <task-list>, which is itself nested inside another container — receive those values, exactly as would happen with any inheritable CSS property in the absence of Shadow DOM.
- CSS variables and
:host() with an attribute selector
:host() with an attribute selectorTo close this lesson, it's worth briefly revisiting the mention made in the last lesson of module 3 about reflect: true and attribute selectors. With the estado property reflected as an attribute (reflect: true, seen in that lesson), it's possible to combine :host() — note the variant with parentheses, different from the plain :host used so far — with an attribute selector, to apply different rules depending on the current value of that attribute, without needing to compute CSS classes dynamically from render():
This rule only applies when the <task-card> element itself has the estado attribute set to exactly "hecha" — which, as explained in module 3, requires estado to be declared with reflect: true so the attribute stays synchronized with the property — slightly fading the whole card once the task is already complete. This technique is only sketched here, as a natural closing of what was explained in the previous module: a more complete treatment of conditionals inside templates, using specific helpers such as classMap or styleMap that offer a more convenient way to toggle classes or inline styles from JavaScript, will be covered in module 7, "Directives and Advanced Template Features." For now, with conventional CSS selectors combined with :host() and the CSS variables from this section, TaskFlow already has a complete, functional theming system.
Common Mistakes and Tips
- Forgetting the default value in
var()and relying on the variable always being defined: if you writecolor: var(--color-hecha);with no second argument, and no ancestor in the document ever defines that variable, thecolorproperty simply doesn't apply (there's no visible error, just the silent absence of the expected style). Declaring the variable on:host, as done in section 5, avoids this problem entirely. - Confusing a CSS variable with a Lit reactive property:
--color-hechahas no relation whatsoever tostatic propertiesor to the reactivity system seen in module 3; it's a pure CSS variable, managed entirely by the browser through the standard cascade and inheritance mechanism, never passing throughrender()orrequestUpdate(). - Trying to customize, through a CSS variable, an aspect that hasn't been deliberately exposed as such: only the variables that the component itself declares and consumes with
var()(like the four in this lesson) can be customized from outside; any other CSS value written directly without going through a variable (like.insignia'sborder-radiusin the section 6 example) remains fully encapsulated and cannot be changed from outside without modifying the component itself. - Declaring the variable on the wrong selector in the outer document: if
--color-hechais defined on a sibling element of<task-card>, instead of on<task-card>itself or on a common ancestor of both, CSS inheritance won't propagate it to the card, because CSS variables follow the same inheritance rules through the document tree as any other inheritable property, not some special "global scope" logic.
Exercises
- Add a new
--color-borde-tarjetavariable to<task-card>, with a default value of#d0d5dd(the same onearticlealready used as a fixed value), and replace the fixed value ofarticle'sborderwithvar(--color-borde-tarjeta). Verify, by defining that variable with a different value in a test page's CSS, that the border of every card changes without touchingtask-card.js. - On the global CSS of a test page containing several
<task-card>elements, declare a:root { --color-en-progreso: #7c3aed; }rule, and verify in the browser that the "en progreso" badges change color across every card on the page at once, without having touched any<task-card>individually. - Explain in your own words, drawing on section 4, why it would be a bad idea to replace this lesson's four color CSS variables with four new reactive properties (
colorPendiente,colorEnProgreso, etc.) declared withstatic properties.
Solutions
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;
}
`,
];With task-card { --color-borde-tarjeta: #93c5fd; } in the test page's CSS, every card now shows a light blue border with no modification whatsoever to task-card.js, through exactly the same CSS variable inheritance mechanism that crosses Shadow DOM explained in section 1.
-
Since
--color-en-progresois declared on:root, the document's root element, and CSS variables are inherited normally by all of its descendants without the Shadow DOM boundary stopping them (section 1), every<task-card>on the page receives that new value through inheritance, and since.insignia--progresoconsumes it withvar(--color-en-progreso)(section 5), all the "en progreso" badges change color simultaneously, with no need to apply the variable individually to each card. -
As explained in section 4, four color reactive properties would mix a purely visual responsibility with the reactive-properties system meant for task data (
titulo,estado,prioridad...); each one would also generate its own HTML attribute and trigger Lit's full update cycle (including a possiblerender()execution) every time it changed, an unnecessary cost for a simple color change. CSS variables solve the same problem at the layer where it belongs, CSS, without ever going through Lit's reactivity mechanism orrender(), and they also integrate naturally with the rest of the standard CSS mechanisms (cascade, inheritance, media queries).
Conclusion
In this lesson you've discovered the deliberate exception that CSS variables represent against Shadow DOM's style encapsulation, and you've learned to declare them with default values on :host to build components that are customizable from outside without breaking that encapsulation in the rest of their CSS. You've applied this mechanism to <task-card>, defining variables for each status color and for the urgency warning, and you've verified how to customize them from the document that uses the component, both individually and through a common ancestor such as :root.
With encapsulated CSS, shared styles, and now theming through variables, TaskFlow already has a complete, coherent style system for the content each component generates from inside its own template. But there's still a different case left to solve: what happens when a component needs to display content it doesn't generate itself, but instead receives from outside, such as the name or picture of the person assigned to a task? That is exactly the content of the next lesson, "Slots and Styling Distributed Content," where you'll get to know the <slot> element and build <user-avatar>, TaskFlow's first component designed to receive distributed content from outside its own shadow root.
Lit Course
Module 1: Introduction to Lit and Web Components
- What are Web Components and why Lit?
- Setting Up the Development Environment
- Your First Lit Component
- Anatomy of a Lit Component
Module 2: Reactive Templates and Rendering
- Lit's Template Engine
- Expressions and Interpolation in Templates
- Conditional Rendering
- List Rendering
- The Rendering Cycle
Module 3: Reactive Properties and State
- Reactive Properties
- Internal State with @state
- Types of Properties and Custom Converters
- Attributes vs Properties and Reflection
Module 4: Styling Lit Components
- Encapsulated CSS with Shadow DOM
- Shared Styles Between Components
- Custom CSS Properties and Theming
- Slots and Styling Distributed Content
Module 5: Events and Component Communication
- Handling DOM Events in Templates
- Custom Events: Communication from Child to Parent
- Communication from Parent to Child with Properties
- Communication Patterns Between Sibling Components
Module 6: Lifecycle and Advanced Behavior
- Lifecycle Callbacks
- Reactive Hooks: willUpdate, updated, and firstUpdated
- Reactive Controllers
- Mixins and Composing Behavior
Module 7: Directives and Advanced Template Features
- Built-in Directives: classMap, styleMap and ifDefined
- Custom Directives
- Asynchronous Rendering with until
- Shared Context with @lit/context
Module 8: Integration, Interoperability and Deployment
- Using Lit Components in Plain HTML
- Integrating Lit with React, Vue, and Angular
- Server-Side Rendering with @lit-labs/ssr
- Bundling, Publishing, and TypeScript
Module 9: Testing and Best Practices
- Unit Tests with Web Test Runner
- Accessibility in Web Components
- Performance and Optimization
- Common Patterns and Anti-patterns
