The previous three lessons have resolved how a Lit component integrates with different consumers: plain HTML, other frameworks, and the server itself before the browser even comes into play. This lesson closes the module with the question that remains once a component —or a complete application like TaskFlow— is ready to leave the development environment: how to bundle it, how to decide whether to publish it as a reusable library or deploy it as an application, and whether or not it's worth migrating the JavaScript code written throughout the course to TypeScript.

Contents

  1. Two different goals: publishing a library versus deploying an application
  2. Bundlers for Lit components: Rollup, Vite, and esbuild
  3. Bundling <task-card> as a component library with Rollup
  4. Deploying TaskFlow as an application with Vite
  5. Bundle size: why Lit is deliberately small
  6. Migrating to TypeScript: decorators versus static properties
  7. 1:1 equivalence: what changes and what doesn't
  8. Closing module 8

  1. Two different goals: publishing a library versus deploying an application

Before talking about specific tools, it's worth distinguishing two scenarios that, although they share vocabulary ("bundling", "build"), pursue different goals and therefore require different configurations. The first scenario is publishing a component library: for example, if <task-card> and <user-avatar> were considered generic and useful enough to publish on npm so other projects —not just TaskFlow— could install them as a dependency. In that case, the goal of bundling is to produce a module that other bundlers can, in turn, import and re-bundle within their own projects, typically without including Lit inside the package itself (letting whoever consumes it bring their own copy of Lit as a shared dependency).

The second scenario is deploying a complete application: TaskFlow as a whole, with all its components already combined, ready to be served directly to end users from a web server or a content delivery network (CDN). Here the goal is precisely the opposite in one key aspect: the final bundle must include everything needed to work on its own —including Lit— optimized for the browser (minified code, splitting into chunks loaded only when needed, cache management by filename hash), without expecting whoever visits it to have to resolve any additional dependency on their own.

This distinction largely determines which tool is appropriate in each case, as seen in the following two sections.

  1. Bundlers for Lit components: Rollup, Vite, and esbuild

Lit's own team recommends Rollup as the reference bundler for publishing component libraries, and that recommendation isn't arbitrary: Rollup is designed, from its origin, with library production as its main use case, generating clean output code close to the input, without the kind of full-page-application-oriented fragmentation other bundlers prioritize. Official tools from Lit's own ecosystem, such as the @lit-labs/starter-kit project generator (or its current equivalent at the time a new project is created), configure Rollup by default precisely for this purpose.

Vite, the bundler used throughout this course for TaskFlow's development project, is the natural alternative for the previous section's second scenario: deploying a complete application. Vite internally uses Rollup for its own production build process (vite build), but adds on top of it all the development experience that has accompanied the course —a hot-reload server, fast module resolution— plus a default configuration already oriented toward applications, not libraries: it splits code into chunks, optimizes static assets, and generates a result ready to serve directly without additional steps.

esbuild frequently comes up as a third option, valued mainly for its build speed, notably higher than Rollup's or older bundlers', thanks to being written in Go instead of JavaScript. Vite, in fact, uses esbuild internally for transformation tasks in its development server (although it falls back to Rollup for the final production build), and esbuild can also be used directly as a standalone bundler for cases where build speed matters more than the fine-grained control over output that Rollup offers.

Bundler Best fit Reason
Rollup Publishing a component library Clean output, close to the source code, designed for other bundlers to reprocess it
Vite Deploying a complete application Development experience already used throughout the course, plus a production configuration oriented toward applications (uses Rollup underneath)
esbuild Cases where build speed is a priority Much faster than JavaScript-based alternatives, at the cost of somewhat less fine-grained control over the output

  1. Bundling <task-card> as a component library with Rollup

If <task-card> were extracted from TaskFlow to be published as an independent npm package, a minimal Rollup configuration could look like this:

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';

export default {
  input: 'src/task-card.js',
  output: {
    file: 'dist/task-card.js',
    format: 'esm',
  },
  plugins: [resolve()],
};

input points to the entry point —the component's own file—, and output.format: 'esm' indicates the output must remain an ES module, the format Lit itself and the rest of the modern JavaScript ecosystem expect to be able to import. The @rollup/plugin-node-resolve plugin lets Rollup correctly resolve the component's own import { LitElement, html, css } from 'lit', locating the lit package inside node_modules, exactly the same kind of module resolution Vite handles transparently throughout the course, but which Rollup needs to configure explicitly through a plugin.

One important detail for this scenario: by default, this configuration does not include Lit's code inside the resulting bundle, since lit appears declared as a dependency (not directly included) in the package.json of the package to be published; whoever installs task-card from npm will also install lit as a transitive dependency, and their own bundler will decide how to combine them, avoiding a situation where two different components from two different packages, both depending on Lit, end up including two separate, independent copies of the same library in the final application consuming both.

  1. Deploying TaskFlow as an application with Vite

For TaskFlow as a complete application, Vite's production build command, already configured by the project created at the start of the course, resolves the opposite scenario:

npm run build

This command runs, underneath, vite build, which uses Rollup to generate a dist/ directory with the final result: minified JavaScript files, with names that include a content hash (useful for browser cache control, since a content change automatically produces a different filename), extracted stylesheets where applicable, and an index.html already pointing to those generated files. Unlike the library scenario from the previous section, here it's actually desirable to include Lit inside the final bundle itself: TaskFlow, as an application, has no external consumer bringing its own copy of Lit; the final bundle must be self-sufficient, ready to be copied to any static file server or CDN and work with no additional dependency to resolve at runtime.

  1. Bundle size: why Lit is deliberately small

Lesson 01-01 already mentioned Lit's small size as one of its advantages over a complete application framework, and this is the moment in the course where that figure gains real practical importance: the smaller the size of the library itself included in the final bundle, the less time the browser takes to download it, parse it, and run it before it can define the first component, a factor that directly affects the "empty page" window described in the previous lesson on SSR, even in applications that don't use SSR at all.

Lit achieves its small size —a few kilobytes gzip-compressed, well below complete application frameworks— through a deliberate design decision, not by chance: Lit consciously limits itself to solving the problem of reactive templates and efficient DOM updates on standard Custom Elements, without including a router of its own, without a global state-management system of its own, without the dozens of additional utilities a complete application framework usually includes by default. Each of those missing pieces —routing, global state— is, precisely, ground TaskFlow has covered throughout the course with its own minimalist tools (@lit/context for shared state, instead of a general-purpose global state-management library), rather than depending on Lit including them out of the box. A bundler like Rollup or Vite can, moreover, apply tree-shaking —removing from the final bundle any part of a library the application code never actually uses—, and Lit's own modular design (functions and classes exported independently, instead of a single monolithic object that includes everything) favors that tree-shaking being effective, further reducing the final size for applications that only use a subset of the available functionality.

  1. Migrating to TypeScript: decorators versus static properties

All of TaskFlow's code, throughout the seven and a half lessons of the course before this module, has been written in plain JavaScript, with static properties as the way to declare reactive properties. Lit offers, optionally and with no obligation to adopt it, an alternative syntax based on TypeScript decorators (or JavaScript with the decorators proposal enabled via Babel), designed for anyone who prefers —or whose project already uses— TypeScript as their main language:

// Version with static properties (JavaScript, the one used throughout the course)
import { LitElement, html } from 'lit';

class TaskCard extends LitElement {
  static properties = {
    titulo: { type: String },
    estado: { type: String },
    expandida: { state: true },
  };

  constructor() {
    super();
    this.expandida = false;
  }

  render() {
    return html`<h3>${this.titulo}</h3>`;
  }
}
// Version with decorators (TypeScript)
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';

@customElement('task-card')
class TaskCard extends LitElement {
  @property({ type: String })
  titulo = '';

  @property({ type: String })
  estado = '';

  @state()
  private expandida = false;

  render() {
    return html`<h3>${this.titulo}</h3>`;
  }
}

Three decorators together cover the same ground that static properties plus customElements.define(...) resolve in the JavaScript version: @customElement('task-card') directly replaces the customElements.define('task-card', TaskCard) call placed at the end of every component file throughout the course, registering the class with that tag name at the same point where the class is declared, instead of on a separate line at the end of the file; @property({...}) on a class field declares that property as reactive and public, with the same configuration options (type, attribute, converter, reflect) already studied since lesson 03-01 for the static properties configuration object; and @state() replaces the { state: true } option, marking the property as internal reactive state, not exposed as a public attribute (as explained in lesson 03-02).

  1. 1:1 equivalence: what changes and what doesn't

The most important point of this section, and the reason this lesson can present TypeScript without needing to repeat anything from the rest of the course, is that migrating to decorators is purely syntactic: it changes no Lit concept studied so far, only the way of writing it. The render cycle (module 2), the reactive property system and its update cycle (module 3), styles with Shadow DOM (module 4), communication between components (module 5), the lifecycle, reactive controllers, and mixins (module 6), and directives including @lit/context (module 7) all work exactly the same, with the same runtime behavior, whether the component was declared with static properties or with decorators. A ReactiveController such as ContadorTiempoRestanteController (lesson 06-03) needs no adaptation to be used from a component written with decorators; a custom directive such as resaltarSiUrgente (lesson 07-02) doesn't either.

What does change is exclusively the way of declaring the class and its properties:

Aspect JavaScript (static properties) TypeScript (decorators)
Registering the element customElements.define('task-card', TaskCard) at the end of the file @customElement('task-card') right above the class
Declaring a public property Entry in the static properties object @property({...}) on the corresponding field
Declaring internal state { state: true } inside static properties @state() on the corresponding field
Initial value of a property Assignment in the constructor Direct assignment in the field declaration (titulo = '')
Type checking None at compile time; only the runtime type conversion (module 3) TypeScript checks, at compile time, that the code uses each property with the declared type

Adopting TypeScript and decorators is, therefore, a decision that can be made independently for each project —or even migrated progressively, component by component, since both syntaxes can coexist within the same application while the transition lasts—, without requiring rewriting any of the design decisions already made throughout the course; the only thing that changes is how those same decisions are expressed in code, plus the added benefit of the type checking TypeScript brings on top, entirely unrelated to Lit itself.

  1. Closing module 8

With this lesson, module 8 is complete. The journey has gone from the inside out: first, how a Lit component is used directly in plain HTML thanks to being, at its core, a standard Custom Element; then, how it integrates inside applications built with other frameworks —React, Vue, and Angular—, each with its own friction points when dealing with Custom Elements; next, how it's rendered on the server with @lit-labs/ssr and Declarative Shadow DOM, so content is visible even before the JavaScript runs; and finally, how it's bundled and published —as a library with Rollup, or as a deployable application with Vite— and what changes, or rather what doesn't change, when optionally migrating to TypeScript.

Common Mistakes and Tips

  • Including Lit inside the bundle of a component library meant to be published on npm: as explained in section 3, this forces any consumer using several components from different packages, all depending on Lit, to load several independent copies of the same library; declaring lit as an external dependency, not included in the bundle itself, avoids that problem.
  • Using the Vite configuration meant for applications when publishing a library, or vice versa: as seen in sections 1 and 2, both scenarios have different goals (self-sufficiency versus external reuse by other bundlers); using the wrong tool for the wrong scenario usually results in an unnecessarily large library bundle, or a deployed application that still expects the browser to resolve dependencies on its own.
  • Mixing static properties and decorators within the same class: although both syntaxes can coexist in different projects, or even in different components of the same project during a progressive migration (section 7), combining both styles within a single class tends to create confusion about which mechanism is actually declaring each property, and it's worth avoiding within a single file.
  • Adopting TypeScript expecting it to change the runtime behavior of any Lit concept: as emphasized in section 7, the migration is purely syntactic; if a component had a design problem in JavaScript (for example, forgetting to propagate Base.properties in a mixin, as warned about in lesson 06-04), that same problem can reappear in TypeScript if it isn't explicitly fixed, since decorators don't automatically replace or correct any of the practices already studied throughout the course.

Exercises

  1. Rewrite the TaskFilter class from lesson 07-04 (with its ContextConsumer and its manejarTexto/manejarEstado methods) using TypeScript decorators instead of static properties, keeping exactly the same behavior.
  2. Explain, based on section 3, what specific problem is avoided by declaring lit as an external dependency (not included in the bundle) when publishing <task-card> as an independent npm package, in a scenario where a consuming application also used another third party's component, equally dependent on Lit.
  3. A teammate, after reading about Rollup and Vite in section 2, proposes using Rollup too for TaskFlow's own production build, instead of vite build, arguing that "it's the tool recommended by the Lit team." Explain why that reasoning isn't correct for TaskFlow's specific case, drawing on the distinction from section 1.

Solutions

import { LitElement, html, css } from 'lit';
import { customElement } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ContextConsumer } from '@lit/context';
import { filtroContext } from '../contexts/filtro-context.js';

@customElement('task-filter')
class TaskFilter extends LitElement {
  private _filtro: ContextConsumer<typeof filtroContext, this>;

  constructor() {
    super();
    this._filtro = new ContextConsumer(this, { context: filtroContext, subscribe: true });
  }

  get valorActual() {
    return this._filtro.value ?? { texto: '', estado: 'todas', actualizar: () => {} };
  }

  manejarTexto(evento: Event) {
    this.valorActual.actualizar({ texto: (evento.target as HTMLInputElement).value });
  }

  manejarEstado(estado: string) {
    this.valorActual.actualizar({ estado });
  }

  render() {
    const { texto, estado } = this.valorActual;
    return html`
      <div class="filtro">
        <input
          type="text"
          placeholder="Buscar tarea…"
          .value="${texto}"
          @input="${this.manejarTexto}"
        />
        <div class="filtro__botones">
          ${['todas', 'pendiente', 'hecha'].map(
            (opcion) => html`
              <button
                class="${classMap({ activo: estado === opcion })}"
                @click="${() => this.manejarEstado(opcion)}"
              >
                ${{ todas: 'Todas', pendiente: 'Pendientes', hecha: 'Hechas' }[opcion]}
              </button>
            `
          )}
        </div>
      </div>
    `;
  }

  static styles = css`
    .filtro__botones button.activo {
      font-weight: bold;
      border-bottom: 2px solid currentColor;
    }
  `;
}

There is no @property nor @state in this class because, just as in lesson 07-04's original version, TaskFilter declares no reactive property of its own: all its visible state (texto, estado) is read directly from this._filtro.value on every render(), exactly as in the JavaScript version.

  1. If <task-card> included its own copy of Lit inside the bundle published on npm, and a third party's component (for example, an <other-widget> from another package, also built with Lit) did the same, an application using both would end up loading two complete, independent copies of Lit in the end user's browser: each with its own code, its own memory footprint, and —more seriously still— with no guarantee that both copies would recognize the same context (@lit/context) or directive instances if, at some point, both components needed to share any of those pieces of infrastructure with each other. By declaring lit as an external dependency of the package, the consuming application's bundler resolves a single shared copy of Lit, installed once in the dependency tree, and both third-party components reuse it with no duplication.
  2. Section 2's Rollup recommendation applies specifically to the scenario of publishing a component library reusable by other projects, not to deploying a complete application like TaskFlow. TaskFlow has no external consumer that will re-bundle its code with their own bundler; its goal is to directly generate the final result served to users, with the entire development experience (hot reload throughout the course) and the application-oriented production configuration (chunk splitting, file-hash cache management) Vite already offers out of the box, and which also uses Rollup underneath for its own production build. Switching to Rollup directly for TaskFlow would bring no advantage over vite build and would only force manually rebuilding much of that application configuration Vite already resolves by default.

Conclusion

With this lesson, module 8 closes: TaskFlow can now integrate into plain HTML, inside other frameworks, render on the server, and be bundled both as a reusable component library and as a deployable application, with the additional option —purely syntactic, changing no concept already studied— of being written in TypeScript with decorators instead of static properties. With TaskFlow now integrable and deployable, what remains is giving it test coverage and reviewing best practices before the final project.

Lit Course

Module 1: Introduction to Lit and Web Components

Module 2: Reactive Templates and Rendering

Module 3: Reactive Properties and State

Module 4: Styling Lit Components

Module 5: Events and Component Communication

Module 6: Lifecycle and Advanced Behavior

Module 7: Directives and Advanced Template Features

Module 8: Integration, Interoperability and Deployment

Module 9: Testing and Best Practices

Module 10: Project: Building TaskFlow

© Copyright 2026. All rights reserved