<task-card> already has its own stylesheet, declared with static styles in the previous lesson. But TaskFlow is an application with several components, and in the next lesson of this course <task-list> will also need its own styles: a container for the board, a title, perhaps a base typography consistent with the cards'. Copy-pasting the same color, typography, and spacing CSS into every new component would mean repeating work and, worse still, would generate inconsistencies as soon as someone updated a color in one component without remembering to update it in the rest as well. This lesson solves that problem by extracting reusable CSS into a shared module and combining it with each component's own styles, applying it to <task-card> and to a newly styled version of <task-list>.
Contents
- The problem: duplicated CSS across several components
- A shared styles module with
css static stylesas an array: combining several sheets- Creating
shared-styles.jsfor TaskFlow - Applying the shared sheet to
<task-card> - Styling
<task-list>: a column layout for the cards - What CSS is worth sharing and what should stay local
- The problem: duplicated CSS across several components
Imagine that, following the same pattern used for <task-card> in the previous lesson, a complete, self-contained static styles were also written for <task-list>, repeating there the same text-color values, the same font family, and the same kind of rounded border that already exist in <task-card>:
// task-card.js
static styles = css`
article {
font-family: system-ui, sans-serif;
color: #1f2933;
border-radius: 8px;
}
`;
// task-list.js — mismo CSS, copiado y pegado
static styles = css`
section {
font-family: system-ui, sans-serif;
color: #1f2933;
border-radius: 8px;
}
`;This approach works, but it has a cost that gets paid as TaskFlow grows: if the font family for the whole application is later changed, or the shade of the main text color is adjusted, that same declaration would need to be located and updated in every component that copied it, with a real risk of missing one and ending up with a visually inconsistent application. It's exactly the same problem that code duplication always raises in programming, now applied to CSS.
The solution, just as with any other repeated piece of code, is to extract that common part to a single place and reuse it wherever it's needed.
- A shared styles module with
css
cssSince css is simply a JavaScript function imported from lit, the value it returns — just like the value html returns — can be stored in a variable, exported from a module, and imported into any other file, exactly as you would with any other JavaScript constant or function.
// src/styles/shared-styles.js
import { css } from 'lit';
export const estilosCompartidos = css`
:host {
font-family: system-ui, sans-serif;
color: #1f2933;
}
`;This file doesn't define any component or class: it's simply a normal JavaScript module whose only content is a constant holding a piece of CSS already "wrapped" by the css function. There's nothing Lit-specific about how it's organized beyond using the css function itself; it's the same "extract to a module and export" pattern you'd already use in any modern JavaScript project to share constants or functions across several files.
static styles as an array: combining several sheets
static styles as an array: combining several sheetsWith the shared CSS already extracted into its own module, each component needs to combine it with its own specific CSS (the border and padding of <task-card>, for instance, which doesn't make sense to share with <task-list>). For this, static styles accepts, in addition to a single css value, an array of several css values, which Lit automatically combines into a single stylesheet applied to the component's shadow root:
import { LitElement, html, css } from 'lit';
import { estilosCompartidos } from '../styles/shared-styles.js';
class TaskCard extends LitElement {
static styles = [
estilosCompartidos,
css`
article {
border: 1px solid #d0d5dd;
border-radius: 8px;
padding: 1rem;
}
`,
];
// ...
}The order within the array matters, just as in any normal CSS stylesheet: rules from the second element of the array apply after those from the first, so if two rules compete exactly on the same specificity over the same selector, the one appearing later in the array wins. In practice, with shared sheets designed not to overlap with each component's specific CSS (as recommended in section 7), this ordering nuance is rarely noticeable, but it's worth keeping in mind if an unexpected styling behavior ever shows up that can't be explained by looking at just one of the two sheets.
- Creating
shared-styles.js for TaskFlow
shared-styles.js for TaskFlowApplying the above to TaskFlow, the shared module can grow a bit beyond the basic typography from section 2, also incorporating the colors and spacing that will repeat across several components of the board:
// src/styles/shared-styles.js
import { css } from 'lit';
export const estilosCompartidos = css`
:host {
display: block;
font-family: system-ui, sans-serif;
color: #1f2933;
}
h2, h3 {
margin: 0 0 0.5rem 0;
color: #1f2933;
}
p {
margin: 0.25rem 0;
color: #52606d;
font-size: 0.9rem;
}
`;Notice that this shared sheet already includes :host { display: block; ... }, which means any component that includes it in its static styles automatically gets the block behavior explained in the previous lesson, without having to repeat that rule separately in each component. It also defines a base text color (#1f2933 for headings, #52606d for secondary text) and a font family, exactly the two values that, in section 1, were identified as clear candidates for duplication between <task-card> and <task-list>.
- Applying the shared sheet to
<task-card>
<task-card>With shared-styles.js already created, <task-card> is updated to use it, removing from its own static styles the rules already covered by the shared sheet:
import { LitElement, html, css } from 'lit';
import { estilosCompartidos } from '../styles/shared-styles.js';
class TaskCard extends LitElement {
static properties = {
// ...same as in the previous lesson...
};
static styles = [
estilosCompartidos,
css`
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;
}
.aviso {
color: #b42318;
font-weight: bold;
}
`,
];
// constructor, alternarExpandida, renderInsigniaEstado, renderFechaLimite and render
// stay exactly the same as in the previous lesson.
}
customElements.define('task-card', TaskCard);Compare this static styles with the one from the previous lesson: the :host, h3, and p rules have disappeared from here because estilosCompartidos now provides them, and only the rules that are truly specific to <task-card> remain — the border and background of article, the look of .insignia and .aviso. The visual result, after this refactoring, is identical to what it already was; what has changed is where each rule comes from, not how the card looks on screen.
- Styling
<task-list>: a column layout for the cards
<task-list>: a column layout for the cardsWith the shared sheet already available, <task-list> can receive its first styles by reusing exactly the same mechanism, while also adding its own layout to arrange the cards in a column:
import { LitElement, html, css } from 'lit';
import { estilosCompartidos } from '../styles/shared-styles.js';
import './task-card.js';
class TaskList extends LitElement {
static properties = {
tareas: { type: Array },
};
static styles = [
estilosCompartidos,
css`
section {
padding: 1rem;
}
.lista {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
`,
];
constructor() {
super();
this.tareas = [
{ id: 1, titulo: 'Preparar la demo del sprint', estado: 'en-progreso', prioridad: 4, urgente: true },
{ id: 2, titulo: 'Revisar el PR de autenticación', estado: 'pendiente', prioridad: 2, urgente: false },
{ id: 3, titulo: 'Desplegar a producción', estado: 'hecha', prioridad: 5, urgente: false },
];
}
render() {
return html`
<section>
<h2>Mis tareas</h2>
<div class="lista">
${this.tareas.map(
(tarea) => html`
<task-card
.titulo="${tarea.titulo}"
.estado="${tarea.estado}"
.prioridad="${tarea.prioridad}"
.urgente="${tarea.urgente}"
></task-card>
`
)}
</div>
</section>
`;
}
}
customElements.define('task-list', TaskList);The .lista class, applied to the <div> that wraps the Array.map producing the cards (already present in the template since module 2), uses display: flex; flex-direction: column; with a gap to visually separate each <task-card> from the next, without needing manual margins on each card. h2 automatically receives the style already defined in estilosCompartidos (the same color and margin also used by <task-card>'s h3), so the "Mis tareas" heading ends up visually consistent with each card's title without a single new rule having been written for it.
It's worth noting an interesting detail about the layout: display: flex is declared on .lista, which lives inside <task-list>'s shadow root, and it affects the <task-card> elements that are direct children of that <div> in the light DOM (the "light," in Web Components terminology) of <task-list>. This works because Flexbox, like any CSS layout property, operates on the actual child elements in the final render tree, regardless of whether those children are native elements or custom elements with their own internal Shadow DOM: <task-list> doesn't need to "see inside" each <task-card> in order to arrange them in a column, in the same way it didn't need to see inside them to pass them their properties either.
- What CSS is worth sharing and what should stay local
Not all the CSS in a multi-component project is a good candidate to live in a shared sheet. A clear criterion is needed to decide what to extract and what to leave in each component's own static styles:
| Good candidate to share | Good candidate to keep local |
|---|---|
Base typography (font-family, general text sizes) |
Layout structure specific to one component (.lista with flex-direction: column in <task-list>) |
| Base text colors (heading, secondary text) | Borders, shadows, or backgrounds that are part of a specific component's visual "shape" (the <task-card> card) |
:host { display: block; }, when it applies to almost every component in the project |
Classes very specific to one component (.insignia, .aviso), which make no sense outside it |
| Very general spacing or border radii, used consistently across the whole application | Any style that depends on data specific to the component (for example, a color that changes based on the estado property, covered in the next lesson) |
The practical rule of thumb is: if changing that value should, at the same time, change the look of several different TaskFlow components, it's a good candidate for shared-styles.js. If it only makes sense for the particular shape or visual behavior of one specific component, it should stay in its own static styles. Pushing too much CSS into the shared module, on the other hand, carries the opposite risk: turning it into a generic, overloaded stylesheet with rules that only one component actually uses, which makes it harder to reason about what CSS affects what.
Common Mistakes and Tips
- Forgetting to wrap the shared CSS with
css, and exporting a plain string instead: ifshared-styles.jsexported simply a string (export const estilosCompartidos = '...';without thecssfunction), Lit wouldn't recognize it as a valid stylesheet when included in thestatic stylesarray, and it would produce a runtime error. Thecssfrom the previous lesson is essential here too. - Accidentally duplicating the same rule in the shared sheet and in a component's local sheet: if
estilosCompartidosalready definescolorforp, and the component declarescolorforpagain in its owncssblock, the second declaration wins (because it comes later in the array, as explained in section 3), which can create a subtle inconsistency that's hard to spot at a glance. It's worth checking, when adding a local rule, that it isn't already covered by the shared sheet. - Putting CSS that only one component uses into the shared sheet: as explained in section 7, pushing too much CSS into
shared-styles.js(for example, the.insigniaclass, which only exists in<task-card>) makes the shared sheet grow in a disorganized way and makes it harder to know, without searching through every component, which rules are actually in use. - Expecting combining sheets in an array to change the CSS application order relative to normal CSS specificity:
static stylesas an array introduces no special priority mechanism beyond the order of appearance and the normal rules of CSS specificity (id, class, tag); it's still ordinary CSS underneath, just split across several fragments that Lit combines before inserting them into the shadow root.
Exercises
- Add a new rule to
shared-styles.jsfor thebuttontag (with future TaskFlow buttons in mind, to be added in later modules), with basic typography and padding, and verify it by applying it temporarily to a test button inside<task-list>. - Intentionally remove the
estilosCompartidosimport intask-card.js(leaving the rest of thestatic stylesarray unchanged) and observe, after reloading the page, what look the card loses. Explain in your own words why exactly that aspect disappears and not another. - Using the criterion from the table in section 7, decide whether a
.detalle { background-color: #f2f4f7; }rule (the expanded detail block in<task-card>, seen in the previous lesson) should be moved toshared-styles.jsor stay in<task-card>'s ownstatic styles. Justify your answer.
Solutions
// src/styles/shared-styles.js
export const estilosCompartidos = css`
:host {
display: block;
font-family: system-ui, sans-serif;
color: #1f2933;
}
h2, h3 {
margin: 0 0 0.5rem 0;
color: #1f2933;
}
p {
margin: 0.25rem 0;
color: #52606d;
font-size: 0.9rem;
}
button {
font-family: inherit;
font-size: 0.9rem;
padding: 0.4rem 0.8rem;
border-radius: 6px;
border: 1px solid #d0d5dd;
cursor: pointer;
}
`;By temporarily adding a <button>Prueba</button> inside <task-list>'s render(), the button already appears with the defined typography and padding, with no need for any additional rule in <task-list>'s own static styles, because both components share the same imported sheet.
-
Without
estilosCompartidosin<task-card>'sstatic stylesarray, the card loses:host { display: block; }(so several cards in a row would go back to sitting on the same line, as explained in the previous lesson) and also loses the text color and base typography defined forh3andp, which would revert to the browser's default style. The border, padding, and background of the<article>, on the other hand, would remain present, because those rules are in the second element of the array, defined locally intask-card.jsrather than in the shared sheet. -
It should stay in
<task-card>'s ownstatic styles. Applying the criterion from section 7:.detalleis a class that only exists inside<task-card>'s template (the block that appears whenexpandidaistrue), it makes no sense to reuse it in<task-list>or in any other future TaskFlow component, and changing its look shouldn't, in principle, affect any other component on the board. It's exactly the profile of "style specific to one component's visual shape" that the table in section 7 recommends keeping local.
Conclusion
In this lesson you've learned how to avoid duplicating CSS across several Lit components by extracting a shared styles module with css, and how to combine it with each component's specific CSS through static styles as an array. You've applied this pattern by creating shared-styles.js for TaskFlow, using it both in <task-card> and in the newly styled version of <task-list>, which now arranges its cards in a column with consistent spacing thanks to Flexbox.
TaskFlow now has, for the first time in the course, a visually coherent look across its two components. But there's still a problem left to solve: the concrete colors — each status badge's color, the urgency warning's color — are written as fixed values inside <task-card>'s CSS, with no way for whoever uses the component from outside to customize them without touching its source code directly. That is exactly the content of the next lesson, "Custom CSS Properties and Theming," where you'll discover why CSS variables are able to cross the Shadow DOM boundary that's been emphasized so much in this lesson, and you'll use them to make TaskFlow's colors configurable from outside each component.
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
