Context

Pass values down a tree without props: setContext and getContext, for JSX, custom elements and plain scopes.

DOM-tree dependency injection: pass values down a custom-element subtree without prop drilling or globals. Provider registers a value on a host element; descendants look it up by walking parentNode (crossing into shadow hosts via getRootNode().host). Innermost provider wins. Reactivity is opt-in: pass a Signal / Computed and read it inside an effect.

setContext

Registers value under key on host. Auto-removed via onCleanup when the surrounding scope disposes — must run inside a reactive scope (an effect, effectScope, the render setup, or a JSX ref callback, which all run inside one).

import { signal } from "elements-kit/signals";
import { setContext } from "elements-kit/utilities/context";

const THEME = Symbol("theme");

function ThemeProvider({ children }: { children: JSX.Element }) {
  const theme = signal<"light" | "dark">("dark");
  return setContext(<div>{children}</div>, THEME, theme);
}

setContext returns the host so it composes inline. The equivalent ref form does the same thing — pick whichever reads better at the call site:

function ThemeProvider({ children }: { children: JSX.Element }) {
  const theme = signal<"light" | "dark">("dark");
  return (
    <div ref={(el) => setContext(el, THEME, theme)}>
      {children}
    </div>
  );
}
function setContext<T>(host: EventTarget, key: PropertyKey, value: T): void

getContext

Walks up from consumer (across open shadow boundaries) and returns the first registered value for key, or undefined. One-shot — does not subscribe. Caller decides how to read the result (call signals inside an effect for reactive reads).

The consumer must be in the DOM tree at call time. Inside a JSX ref callback the element has not yet been inserted, so a synchronous getContext(el, …) returns undefined. Defer the lookup to mount time with <dom-lifecycle>:

import { signal } from "elements-kit/signals";
import { getContext } from "elements-kit/utilities/context";
import "elements-kit/utilities/dom-lifecycle";

function ThemedButton() {
  const theme = signal<"light" | "dark" | undefined>(undefined);
  return (
    <button data-theme={() => theme() ?? "light"}>
      <dom-lifecycle
        onConnect={(el) => theme(getContext<"light" | "dark">(el, THEME))}
      />
      Click
    </button>
  );
}

For one-off reads, call getContext directly from an event handler — the tree is always connected by the time the handler fires:

function ThemedButton() {
  return (
    <button
      on:click={(e) => {
        const theme = getContext<() => "light" | "dark">(e.currentTarget, THEME);
        console.log("theme:", theme?.());
      }}
    >
      Click
    </button>
  );
}
function getContext<T>(consumer: Element, key: PropertyKey): T | undefined

Consuming context from a component

For a component body that needs getContext after connection, defer with <dom-lifecycle> — it's render-inert (zero layout box, no a11y role) and its onConnect fires once the consumer is in the tree:

import { signal } from "elements-kit/signals";
import { getContext } from "elements-kit/utilities/context";
import "elements-kit/utilities/dom-lifecycle";

function ThemeBadge() {
  const theme = signal<"light" | "dark" | undefined>(undefined);
  return (
    <dom-lifecycle
      onConnect={(el) => theme(getContext<"light" | "dark">(el, THEME))}
    >
      <span>{() => theme() ?? "light"}</span>
    </dom-lifecycle>
  );
}

For a Signal/Computed-valued context that should drive reactivity, read the resolved signal inside an effect.

Nesting

Innermost provider wins because the walk returns on first hit:

<ThemeProvider>
  <ThemedButton />            {/* outer theme */}
  <ThemeProvider>
    <ThemedButton />          {/* inner theme */}
  </ThemeProvider>
</ThemeProvider>

Static values

Reactivity is opt-in — plain values work the same way:

const CONFIG = Symbol("config");

function ConfigProvider({ children }: { children: JSX.Element }) {
  return setContext(<div>{children}</div>, CONFIG, { apiUrl: "/v1" });
}

Custom elements

Inside a custom element, setContext(this, ...) registers on the host directly — no wrapper needed. Use the render(this, template) lifecycle pattern so the entry is auto-cleaned on disconnect:

import { render } from "elements-kit/render";

class ThemeProvider extends HTMLElement {
  #unmount?: () => void;
  #template = () => {
    const theme = signal<"light" | "dark">("dark");
    setContext(this, THEME, theme);
    return null;
  };
  connectedCallback() { this.#unmount = render(this, this.#template); }
  disconnectedCallback() { this.#unmount?.(); this.#unmount = undefined; }
}

See also