Skip to main content
Lit provides several packages that ship reactive controllers wrapping common browser and application APIs. Install only what you need.

@lit-labs/observers

ResizeController, MutationController, IntersectionController, PerformanceController

@lit/task

Task controller for async operations like data fetching

@lit/context

ContextConsumer and ContextProvider for cross-component data sharing

@lit-labs/observers

The @lit-labs/observers package integrates the four browser Observer APIs with the reactive update lifecycle.
npm install @lit-labs/observers

How observer controllers work

All four controllers follow the same pattern:
  1. Construct the controller with the host element and a config object.
  2. Provide a callback function that converts raw observer entries into a typed value.
  3. Read controller.value in the host’s render() — it is always up to date.
  4. The controller automatically starts observing on hostConnected and stops on hostDisconnected.

ResizeController

ResizeController wraps the ResizeObserver API to notify the host whenever a target element’s size changes. Import
import { ResizeController } from '@lit-labs/observers/resize-controller.js';
Constructor
new ResizeController<T>(host: ReactiveControllerHost & Element, config: ResizeControllerConfig<T>)
Config options
OptionTypeDescription
configResizeObserverOptionsOptions passed directly to ResizeObserver.observe().
targetElement | nullElement to observe. Defaults to the host element. Pass null to observe nothing automatically.
callback(entries: ResizeObserverEntry[], observer: ResizeObserver) => TProcesses observer entries into controller.value.
skipInitialbooleanWhen true, skips calling callback with an empty entry list on first observe. Defaults to false.
Instance members
MemberTypeDescription
valueT | undefinedThe last value returned by callback.
callbackResizeValueCallback<T> | undefinedThe callback function (can be reassigned).
observe(target)voidStart observing an additional element.
unobserve(target)voidStop observing an element.
target(observe?)directiveElement directive that auto-observes the element it is applied to.
Example
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { ResizeController } from '@lit-labs/observers/resize-controller.js';

@customElement('my-resizable')
class MyResizable extends LitElement {
  private _resizeController = new ResizeController(this, {
    callback: ([entry]) => entry?.contentRect,
  });

  render() {
    const rect = this._resizeController.value;
    return html`
      <p>Width: ${rect?.width ?? 0}px</p>
      <p>Height: ${rect?.height ?? 0}px</p>
    `;
  }
}
You can also observe a specific child element using the target directive:
@customElement('my-resizable')
class MyResizable extends LitElement {
  private _resizeController = new ResizeController(this, {
    target: null, // don't auto-observe the host
    callback: ([entry]) => entry?.contentRect,
  });

  render() {
    return html`
      <div ${this._resizeController.target()}>
        <p>Width: ${this._resizeController.value?.width ?? 0}px</p>
      </div>
    `;
  }
}
ResizeController logs a warning and exits early in environments where ResizeObserver is not available (including server-side rendering).

MutationController

MutationController wraps the MutationObserver API to notify the host when DOM mutations occur on a target element. Import
import { MutationController } from '@lit-labs/observers/mutation-controller.js';
Constructor
new MutationController<T>(host: ReactiveControllerHost & Element, config: MutationControllerConfig<T>)
Config options
OptionTypeDescription
configMutationObserverInitRequired. Options passed directly to MutationObserver.observe() (e.g. { childList: true, attributes: true }).
targetElement | nullElement to observe. Defaults to the host element. Pass null to observe nothing automatically.
callback(records: MutationRecord[], observer: MutationObserver) => TProcesses mutation records into controller.value.
skipInitialbooleanWhen true, skips calling callback with an empty record list on first observe. Defaults to false.
Instance members
MemberTypeDescription
valueT | undefinedThe last value returned by callback.
callbackMutationValueCallback<T> | undefinedThe callback function (can be reassigned).
observe(target)voidStart observing an additional element.
Example Detect when child nodes are added or removed from the host element:
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { MutationController } from '@lit-labs/observers/mutation-controller.js';

@customElement('my-list')
class MyList extends LitElement {
  private _mutationController = new MutationController(this, {
    config: { childList: true, subtree: true },
    callback: (records) => records.flatMap((r) => [...r.addedNodes]),
  });

  render() {
    const added = this._mutationController.value ?? [];
    return html`
      <slot></slot>
      <p>${added.length} node(s) added since last mutation.</p>
    `;
  }
}
MutationController logs a warning and exits early in environments where MutationObserver is not available, including server-side rendering.

IntersectionController

IntersectionController wraps the IntersectionObserver API to notify the host when a target element enters or exits the viewport (or another scroll container). Import
import { IntersectionController } from '@lit-labs/observers/intersection-controller.js';
Constructor
new IntersectionController<T>(host: ReactiveControllerHost & Element, config: IntersectionControllerConfig<T>)
Config options
OptionTypeDescription
configIntersectionObserverInitOptions passed directly to the IntersectionObserver constructor (e.g. { threshold: 0.5 }).
targetElement | nullElement to observe. Defaults to the host element. Pass null to observe nothing automatically.
callback(entries: IntersectionObserverEntry[], observer: IntersectionObserver) => TProcesses entries into controller.value.
skipInitialbooleanWhen true, ignores the initial intersection state reported when observation starts. Defaults to false.
Instance members
MemberTypeDescription
valueT | undefinedThe last value returned by callback.
callbackIntersectionValueCallback<T> | undefinedThe callback function (can be reassigned).
observe(target)voidStart observing an additional element.
unobserve(target)voidStop observing an element.
Example Show different content based on whether the element is visible in the viewport:
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { IntersectionController } from '@lit-labs/observers/intersection-controller.js';

@customElement('lazy-image')
class LazyImage extends LitElement {
  private _intersectionController = new IntersectionController(this, {
    config: { threshold: 0 },
    callback: ([entry]) => entry?.isIntersecting ?? false,
    skipInitial: false,
  });

  render() {
    const visible = this._intersectionController.value;
    return html`
      ${visible
        ? html`<img src="/image.jpg" alt="Lazy loaded image" />`
        : html`<div class="placeholder"></div>`}
    `;
  }
}
Unlike ResizeController and MutationController, IntersectionObserver always fires its callback immediately when observe() is called to report the initial intersection state. Use skipInitial: true to suppress this if the initial state is not needed.
IntersectionController logs a warning and exits early in environments where IntersectionObserver is not available, including server-side rendering.

PerformanceController

PerformanceController wraps the PerformanceObserver API to collect performance timeline entries (marks, measures, resource timings, etc.) and make them available in the reactive update cycle. Import
import { PerformanceController } from '@lit-labs/observers/performance-controller.js';
Constructor
new PerformanceController<T>(host: ReactiveControllerHost, config: PerformanceControllerConfig<T>)
Unlike the other observer controllers, PerformanceController accepts a plain ReactiveControllerHost — it does not require the host to also be an Element, since PerformanceObserver is not tied to a specific DOM element.
Config options
OptionTypeDescription
configPerformanceObserverInitRequired. Options passed to PerformanceObserver.observe() (e.g. { entryTypes: ['measure'] }).
callback(entries: PerformanceEntryList, observer: PerformanceObserver, entryList?: PerformanceObserverEntryList) => TProcesses performance entries into controller.value.
skipInitialbooleanWhen true, skips calling callback with an empty entry list on first observe. Defaults to false.
Instance members
MemberTypeDescription
valueT | undefinedThe last value returned by callback.
callbackPerformanceValueCallback<T> | undefinedThe callback function (can be reassigned).
observe()voidStart observing. Called automatically on hostConnected.
flush()voidImmediately process any buffered entries and request a host update.
Example Collect measure entries and display the most recent one:
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { PerformanceController } from '@lit-labs/observers/performance-controller.js';

@customElement('perf-monitor')
class PerfMonitor extends LitElement {
  private _perf = new PerformanceController(this, {
    config: { entryTypes: ['measure'] },
    callback: (entries) => entries.at(-1),
  });

  render() {
    const latest = this._perf.value;
    return html`
      <p>
        Last measure: ${latest ? `${latest.name}${latest.duration.toFixed(1)}ms` : 'none'}
      </p>
    `;
  }
}
PerformanceController logs a warning and exits early in environments where PerformanceObserver is not available, including server-side rendering.

Other built-in controllers

Task

@lit/task — The Task controller manages async operations with built-in pending, complete, and error states. Use it for data fetching, debounced computations, and any work that should re-run when reactive properties change.

ContextConsumer and ContextProvider

@lit/contextContextConsumer and ContextProvider implement the W3C community Context Protocol, enabling components to share data across the element tree without prop drilling.