Skip to main content

Overview

Passing data deep into a component tree requires threading props through every intermediate element — even those that don’t use the data themselves. @lit/context solves this with the W3C Web Components Community Group Context Protocol: a provider element makes a value available, and any descendant can consume it directly, regardless of nesting depth. Under the hood the protocol uses a bubbling context-request CustomEvent. A consumer dispatches the event; the nearest ancestor provider intercepts it, stops propagation, and invokes the consumer’s callback with the current value.

Installation

npm install @lit/context

Core concepts

createContext()

createContext stamps a typed key that ties providers to consumers. Two calls with the same primitive key (string, Symbol.for) return the same context; two calls with distinct keys (plain object, Symbol()) are always separate.
import {createContext} from '@lit/context';

export interface Theme {
  primaryColor: string;
  fontFamily: string;
}

// String key — shared across any module that imports the same string
export const themeContext = createContext<Theme>('theme');
// Symbol.for — also shared by key name
export const themeContext = createContext<Theme>(Symbol.for('theme'));

// Symbol() — unique, never collides
export const themeContext = createContext<Theme>(Symbol('theme'));
Signature:
function createContext<ValueType, K = unknown>(key: K): Context<K, ValueType>

ContextProvider controller

ContextProvider is a ReactiveController that listens for context-request events on its host element and responds with the current value. It can be attached to any HTMLElement; for Lit elements it registers itself automatically.
new ContextProvider(host: HostElement, options: {
  context: C;
  initialValue?: ContextType<C>;
})
To update the provided value after construction, call provider.setValue(newValue).

ContextConsumer controller

ContextConsumer dispatches a context-request event when its host connects and stores the received value on consumer.value.
new ContextConsumer(host: HostElement, options: {
  context: C;
  callback?: (value: ContextType<C>, dispose?: () => void) => void;
  subscribe?: boolean; // receive future updates from the provider
})
Set subscribe: true to keep receiving updates whenever the provider calls setValue.

@provide() decorator

The @provide decorator creates a ContextProvider controller and wires it to a class accessor property. Assigning the property automatically calls setValue on the underlying controller.
import {provide} from '@lit/context';
import {themeContext, Theme} from './theme-context.js';

class MyApp extends LitElement {
  @provide({context: themeContext})
  theme: Theme = {primaryColor: '#344cfc', fontFamily: 'sans-serif'};
}
Signature:
function provide<ValueType>({
  context,
}: {
  context: Context<unknown, ValueType>;
}): ProvideDecorator<ValueType>

@consume() decorator

The @consume decorator creates a ContextConsumer controller and updates the decorated property when the context value changes.
import {consume} from '@lit/context';
import {themeContext, Theme} from './theme-context.js';

class MyButton extends LitElement {
  @consume({context: themeContext, subscribe: true})
  theme?: Theme;
}
Signature:
function consume<ValueType>({
  context,
  subscribe,
}: {
  context: Context<unknown, ValueType>;
  subscribe?: boolean;
}): ConsumeDecorator<ValueType>

Complete example

This example builds a theme system: <my-app> provides the theme, and a deeply nested <my-button> consumes it.
1

Define the context key

// theme-context.ts
import {createContext} from '@lit/context';

export interface Theme {
  primaryColor: string;
  fontFamily: string;
}

export const themeContext = createContext<Theme>('theme');
2

Provide the context

// my-app.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {provide} from '@lit/context';
import {themeContext, Theme} from './theme-context.js';

@customElement('my-app')
class MyApp extends LitElement {
  @provide({context: themeContext})
  @property({attribute: false})
  theme: Theme = {
    primaryColor: '#344cfc',
    fontFamily: 'system-ui',
  };

  render() {
    return html`
      <my-button></my-button>
      <button @click=${this._toggleTheme}>Switch theme</button>
    `;
  }

  private _toggleTheme() {
    this.theme = {
      primaryColor: this.theme.primaryColor === '#344cfc' ? '#e63946' : '#344cfc',
      fontFamily: this.theme.fontFamily,
    };
  }
}
3

Consume the context

// my-button.ts
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';
import {consume} from '@lit/context';
import {themeContext, Theme} from './theme-context.js';

@customElement('my-button')
class MyButton extends LitElement {
  @consume({context: themeContext, subscribe: true})
  theme?: Theme;

  render() {
    return html`
      <button style="
        background: ${this.theme?.primaryColor};
        font-family: ${this.theme?.fontFamily};
      ">
        <slot></slot>
      </button>
    `;
  }
}

Using the controller directly

When decorators aren’t available, use the controllers directly:
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ContextProvider} from '@lit/context';
import {themeContext, Theme} from './theme-context.js';

@customElement('my-app')
class MyApp extends LitElement {
  private _provider = new ContextProvider(this, {
    context: themeContext,
    initialValue: {primaryColor: '#344cfc', fontFamily: 'system-ui'},
  });

  updateTheme(theme: Theme) {
    this._provider.setValue(theme);
  }

  render() {
    return html`<my-button></my-button>`;
  }
}

Providing context from a plain element

ContextProvider works with any HTMLElement, not just custom elements. This is useful for providing context at the document or app level without introducing an extra custom element:
import {ContextProvider} from '@lit/context';
import {themeContext} from './theme-context.js';

const provider = new ContextProvider(document.body, {
  context: themeContext,
  initialValue: {primaryColor: '#344cfc', fontFamily: 'system-ui'},
});

// Manually call hostConnected() if consumers may already exist in the DOM
provider.hostConnected();

Handling late-upgraded providers

If a provider element upgrades after consumers have already connected, those consumers will miss the initial context-request event. ContextRoot solves this by collecting unsatisfied requests and re-dispatching them when a new provider connects:
import {ContextRoot} from '@lit/context';

const root = new ContextRoot();
root.attach(document.body);
ContextRoot uses WeakRef, which requires a modern browser. It is not supported in IE11.

How events work

The protocol uses two custom events, both composed and bubbling:
EventDirectionPurpose
context-requestConsumer → ancestorRequests a context value
context-providerProvider → windowAnnounces a provider has connected
When subscribe: true, the provider passes an unsubscribe function to the consumer’s callback. The consumer stores it and calls it on disconnect, or when a closer provider takes over.

Build docs developers (and LLMs) love