Skip to content

Instantly share code, notes, and snippets.

@oscarmarina
Last active April 24, 2025 15:02
Show Gist options
  • Save oscarmarina/b2ecf25542072ea0d73f3fa24537591d to your computer and use it in GitHub Desktop.
Save oscarmarina/b2ecf25542072ea0d73f3fa24537591d to your computer and use it in GitHub Desktop.
`BaseContextMetaElement` is inspired by the concept of Customized Built-in Elements, focusing on extending native HTML elements like `div` using Lit's features and the Context API.

Lit

Emulating CSS Style Queries with Lit Context

context-meta-provider

Exercise to emulate CSS Style Queries using Context.

Currently, Style Queries are only supported in Chromium and Safari, but there seems to be a possible bug in Safari with elements inside a <slot>.


context-meta.js

It is a Lit Reactive Controller that encapsulates the controllers provided by @lit/context.

context-meta-provider.js A Lit directive that allows normal DOM elements to act as context providers. You can use this directive in both attribute and element bindings in Lit templates.

flow-element.js Emulates the behavior of a flow element using ARIA, keeping standard HTML functionality while adding improved features.

Change the default role for custom elements


Links:


Demos(stackblitz):

main branch:

native-container-style-queries branch:

This branch demonstrates native container style queries, illustrating behavior with full native support:

import {ContextMeta} from './context-meta.js';
export const contextMetaKeyProvider = Symbol();
export const cacheContextMetaProvider = (element, contextOrOptions) => {
if (element[contextMetaKeyProvider]) {
return element[contextMetaKeyProvider];
}
const ctx =
contextOrOptions?.context !== undefined ? {...contextOrOptions} : {context: contextOrOptions};
element[contextMetaKeyProvider] = new ContextMeta(element, ctx);
return element[contextMetaKeyProvider];
};
export const consumerContext = Symbol.for('symbol-surface');
import {LitElement, html, css, nothing} from 'lit';
import {ContextMeta} from './context-meta.js';
import {consumerContext} from './consumer-context.js';
class ConsumerElement extends LitElement {
static styles = css`
:host {
display: block;
padding: 0.5rem 1rem;
margin: 1rem 0;
background-color: #ffd28d;
color: #432c00;
}
:host([hidden]),
[hidden] {
display: none !important;
}
:host([surface='dim']) {
background-color: #cee36a;
color: #2c3400;
}
`;
static properties = {
surface: {reflect: true},
};
constructor() {
super();
this.propertyContext = new ContextMeta(this, {
context: consumerContext,
callback: (v) => (this.surface = v),
});
}
render() {
return html`
<p data-surface="${this.propertyContext.value ?? nothing}"><slot></slot></p>
`;
}
}
customElements.define('consumer-element', ConsumerElement);
import {Directive, directive, PartType} from 'lit/directive.js';
import {noChange} from 'lit';
import {cacheContextMetaProvider, contextMetaKeyProvider} from './cache-context-meta-provider.js';
/**
* `contextMetaProviderDirective` is a Lit directive that enables normal DOM elements to act as context providers.
* You can use this directive in both attribute and element bindings in Lit templates.
*
* > https://github.com/lit/lit/discussions/4690
*
* ## Features
* - Enables non-Lit elements to provide context.
* - Works seamlessly with [`@lit/context`](https://lit.dev/docs/data/context/).
* - Utilizes `context-meta`, a Lit Reactive Controller for managing context.
*
* ```js
* <div ${contextMetaProviderDirective(myContext, someValue)}>
* <!-- Children can consume the provided context -->
* </div>
* //
* <div data-info="${contextMetaProviderDirective(myContext, someValue)}">
* <!-- Children can consume the provided context -->
* </div>
* ```
*/
class ContextMetaProviderDirective extends Directive {
/** @type {*} */
#partInfo = undefined;
#currentValue = undefined;
/**
* @param {import('lit/directive.js').PartInfo} partInfo - Information about the part this directive is bound to
*/
constructor(partInfo) {
super(partInfo);
if (partInfo.type !== PartType.ATTRIBUTE && partInfo.type !== PartType.ELEMENT) {
throw new Error(
'contextMetaProviderDirective can only be used in an attribute or element directive.'
);
}
this.#partInfo = partInfo;
}
/**
* Main render method called by Lit.
* @param {*} value - The context value to provide.
* @param {{
* context?: *,
* initialValue?: import('@lit/context').ContextType<*>,
* }} context - The context object.
* @returns {unknown} - The serialized context value or noChange.
*/
render(value, context) {
if (value !== this.#currentValue) {
this.#currentValue = value;
this.updateValue(value, context);
return this.resolveAttrValue();
}
return noChange;
}
/**
* Updates the context value for the element.
* @param {*} value - The new context value.
* @param {{
* context?: *,
* initialValue?: import('@lit/context').ContextType<*>,
* }} context - The context object.
*/
updateValue(value, context) {
const element = this.#partInfo.element;
cacheContextMetaProvider(element, context);
element[contextMetaKeyProvider].setValue(value);
}
/**
* Decides whether to return the currentValue if the directive is used as an attribute.
* @returns {unknown} - The context value or noChange.
*/
resolveAttrValue() {
if (this.#partInfo.type !== PartType.ATTRIBUTE) {
return noChange;
}
return this.#currentValue;
}
}
export const contextMetaProvider = directive(ContextMetaProviderDirective);
import {createContext, ContextProvider, ContextConsumer} from '@lit/context';
export class ContextMeta {
/**
* @param {import('lit').ReactiveElement} host - The host object.
* @param {{
* context?: *,
* initialValue?: import('@lit/context').ContextType<*>,
* callback?: (value: import('@lit/context').ContextType<*>, dispose?: () => void) => void
* }} arg - The arguments for the constructor.
* @param {boolean} isConsumerOnly - If true, the controller will only be a consumer. Default is false.
*/
constructor(host, {context = contextMetaSymbol, initialValue, callback}, isConsumerOnly = false) {
this.context = createContext(context);
this.initialValue = initialValue;
this.callback = callback;
this.host = host;
if (!isConsumerOnly) {
this._contextMetaProvider = new ContextProvider(this.host, {
context: this.context,
initialValue: this.initialValue,
});
}
this.host.addController?.(this);
}
get value() {
return this._contextMetaConsumer?.value;
}
setValue(v, force = false) {
this._contextMetaProvider?.setValue?.(v, force);
}
async hostConnected() {
await this.host.updateComplete;
// Await possible asynchronous completion of the host's update lifecycle
window.queueMicrotask(() => {
this._contextMetaConsumer = new ContextConsumer(this.host, {
context: this.context,
subscribe: true,
callback: this.callback,
});
});
}
}
import {LitElement, html, css} from 'lit';
import {ContextMeta} from './context-meta.js';
import {consumerContext} from './consumer-context.js';
class BaseElement extends LitElement {
static styles = css`
:host {
display: block;
}
:host([hidden]),
[hidden] {
display: none !important;
}
`;
connectedCallback() {
super.connectedCallback?.();
Object.assign(this, {role: this.role ?? 'none'});
}
render() {
return html`
<slot></slot>
`;
}
}
class FlowElement extends BaseElement {
static properties = {
surface: {state: true},
};
constructor() {
super();
this.propertyContext = new ContextMeta(this, {
context: consumerContext,
});
}
willUpdate(props) {
super.willUpdate?.(props);
if (props.has('surface')) {
this.propertyContext.setValue(this.surface);
}
}
}
customElements.define('flow-element', FlowElement);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="data:;base64,_" />
<title>context demo</title>
<script type="module" src="./provider-element.js"></script>
<script type="module" src="./consumer-element.js"></script>
<style>
:root {
font: normal medium/1.25 sans-serif;
}
:not(:defined) {
visibility: hidden;
}
body {
margin: 0;
padding: 1rem 2rem;
}
hr {
border: none;
background: linear-gradient(to right, transparent, hsla(0, 0%, 60%, 0.5), transparent);
height: 0.0625rem;
margin: 1rem 0;
}
button {
display: inline-block;
padding: 0.5rem 1rem;
margin: 1rem 0 0;
}
button span {
pointer-events: none;
}
</style>
</head>
<body>
<button type="button">
Toggle surface
<span></span>
</button>
<hr />
<provider-element surface="bright">
<consumer-element>Consumer in slot</consumer-element>
<consumer-element slot="containerA">Consumer in slot</consumer-element>
<consumer-element slot="containerB">Consumer in slot</consumer-element>
<consumer-element slot="nested-container">Consumer in slot</consumer-element>
</provider-element>
<script>
const button = document.querySelector('button');
const provider = document.querySelector('provider-element');
button.addEventListener('click', ({target}) => {
provider.surface = provider.surface === 'dim' ? 'bright' : 'dim';
target.children[0].textContent = provider.surface;
});
</script>
</body>
</html>
{
"scripts": {
"start": "vite"
},
"dependencies": {
"lit": "^3.2.1",
"@lit/context": "^1.1.4",
"@lit/reactive-element": "^2.0.4",
"lit-element": "^4.1.1",
"lit-html": "^3.2.1"
},
"devDependencies": {
"vite": "^6.2.3"
}
}
import {LitElement, html, css, nothing} from 'lit';
import {ContextMeta} from './context-meta.js';
import {consumerContext} from './consumer-context.js';
import {contextMetaProvider} from './context-meta-provider.js';
import './flow-element.js';
import './consumer-element.js';
const handleSurface = (surface) => {
switch (surface) {
case 'dim':
return 'bright';
default:
return 'dim';
}
};
class ProviderElement extends LitElement {
static styles = css`
:host {
display: block;
padding: 0.5rem 1rem;
contain: content;
}
:host([hidden]),
[hidden] {
display: none !important;
}
.container,
.nested-container {
padding: 1rem;
}
:host(:not([surface='dim'])),
:host([surface='dim']) .container,
:host(:not([surface='dim'])) .nested-container {
background-color: #e5a427;
color: #342100;
}
:host([surface='dim']),
:host(:not([surface='dim'])) .container,
:host([surface='dim']) .nested-container {
background-color: #543b0f;
color: #ede1d3;
}
hr {
border: none;
background: linear-gradient(to right, transparent, #766043, transparent);
height: 0.0625rem;
margin: 1rem 0;
}
`;
static properties = {
surface: {reflect: true},
};
constructor() {
super();
this.surface = undefined;
this.propertyContext = new ContextMeta(this, {
context: consumerContext,
});
}
willUpdate(props) {
super.willUpdate?.(props);
if (props.has('surface')) {
this.propertyContext.setValue(this.surface);
}
}
render() {
return html`
<p>Provider element</p>
<consumer-element>Consumer in Shadow DOM</consumer-element>
<slot></slot>
<hr />
<div
id="A-div"
class="container"
${contextMetaProvider(handleSurface(this.surface), {
context: consumerContext,
})}>
<p>
<code>native div element:</code>
container - provider element
</p>
<consumer-element>Consumer in Shadow DOM</consumer-element>
<slot name="containerA"></slot>
</div>
<hr />
<div
id="B-div"
class="container"
data-surface="${contextMetaProvider(
handleSurface(this.surface),
/** @type {*} */ (consumerContext)
)}">
<p>
<code>native div element:</code>
container - provider element
</p>
<consumer-element>Consumer in Shadow DOM</consumer-element>
<slot name="containerB"></slot>
<flow-element id="C-div" class="nested-container" .surface="${this.surface}">
<p>
<code>flow-element:</code>
nested container - provider element
</p>
<slot name="nested-container"></slot>
<consumer-element>Consumer in Shadow DOM</consumer-element>
</flow-element>
</div>
<hr />
<consumer-element>Consumer in Shadow DOM</consumer-element>
`;
}
}
customElements.define('provider-element', ProviderElement);
{
"files": {
"README.md": {
"position": 0
},
"index.html": {
"position": 1
},
"provider-element.js": {
"label": "Provider",
"position": 2
},
"consumer-element.js": {
"label": "Consumer",
"position": 3
},
"flow-element.js": {
"label": "Flow Element",
"position": 4
},
"context-meta.js": {
"label": "ContextMeta",
"position": 5
},
"context-meta-provider.js": {
"label": "Directive",
"position": 6
},
"cache-context-meta-provider.js": {
"label": "CacheDirective",
"position": 7
},
"consumer-context.js": {
"label": "consumerContext",
"position": 8
},
"package.json": {
"position": 9,
"hidden": true
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment