Skip to content

Runtime API

Two runtime entry points ship with the package: the CSS helpers under @oddsquad/vite-plugin-lit/css.js, and the timeline API behind virtual:lit-plugin/timeline. Both are dependency-free and safe to import from component code.

function urlSheet(url: string): UrlSheet;

Builds one constructed CSSStyleSheet from a ?url-imported CSS asset. Adopt it from any number of shadow roots through adoptedStyleSheets, and an edit to the source CSS re-fetches and replaceSync()s that one object. Every root that adopted it restyles, with no component re-render and no page reload.

src/styles/utility-sheet.ts
import {urlSheet} from '@oddsquad/vite-plugin-lit/css.js';
import sheetUrl from './utils.css?url';
const {sheet, onHotUpdate} = urlSheet(sheetUrl);
import.meta.hot?.accept('./utils.css?url', onHotUpdate);
export default sheet;

Register onHotUpdate in the module that imports the CSS, with the same literal specifier. Vite resolves accepted dependencies by static analysis, so the string has to appear in your source. Overlapping fetches from rapid edits resolve in issue order, so a slow earlier request cannot clobber a newer one.

interface UrlSheet {
sheet: CSSStyleSheet;
onHotUpdate: (mod: Record<string, unknown> | undefined) => void;
}

sheet is empty until the first fetch resolves, so the page shows a brief flash of unstyled content on initial load. That is the cost of keeping the CSS a real asset instead of inlining it into the JS bundle. onHotUpdate is typed to match Vite’s accept callback, so it drops straight into import.meta.hot?.accept().

function devCacheBust(url: string): string;

Appends a cache-busting query to a ?url-imported CSS file URL in dev. Each HMR re-execution then yields a fresh href, and the browser refetches the changed stylesheet. Production builds already serve a content-hashed asset, so the input comes back unchanged.

src/my-element.ts
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';
import {devCacheBust} from '@oddsquad/vite-plugin-lit/css.js';
import cssUrl from './my-element.css?url';
const href = devCacheBust(cssUrl);
@customElement('my-element')
export class MyElement extends LitElement {
override render() {
return html`<link rel="stylesheet" href=${href} />
<p>Styled by a linked sheet.</p>`;
}
}
function addTimelineLayer(layer: TimelineLayer): void;
interface TimelineLayer {
id: string;
label: string;
/** 0xRRGGBB */
color: number;
}

Registers a layer, one category of recorded timeline events with its own colour and toggle. The panel appends it to the toggle strip after the built-in layers. Duplicate ids are ignored, so calling it on every module evaluation is safe.

function addTimelineEvent(event: TimelineEventInput): void;
interface TimelineEventInput<TData = unknown> {
layerId: string;
/** A `performance.now()` value. Leave it out to stamp the event now. */
time?: number;
data: TData;
title?: string;
subtitle?: string;
/** Pairs a start event with its matching end event. */
groupId?: number | string;
logType?: 'default' | 'warning' | 'error';
meta?: {
elementId?: number;
tagName?: string;
source?: {file: string; line: number};
};
}

Emits one event onto a custom layer or onto a built-in id: 'lit-lifecycle', 'lit-render', 'lit-render-verbose', 'mouse', 'keyboard', 'custom-events', or 'lit-warnings'.

src/fetch-layer.ts
import {addTimelineEvent, addTimelineLayer} from 'virtual:lit-plugin/timeline';
addTimelineLayer({id: 'fetch', label: 'Fetch', color: 0x22d3ee});
export const recordFetch = (url: string, ms: number): void => {
addTimelineEvent({
layerId: 'fetch',
time: performance.now(),
data: {url, ms},
title: url,
subtitle: `${ms.toFixed(1)} ms`,
});
};

color is a number, not a CSS string. data is serialized before it reaches the panel, so keep it to plain values. meta is filled in by the plugin’s own capture layers and custom events have no reason to set it. The panel shows every event on the recording’s clock, milliseconds since Record; the plugin moves a custom event’s performance.now() time onto it, so the event lines up with the Lit updates around it.

The virtual module resolves to a no-op stub in any vite build, including a build with timeline enabled, because the events only ever travel over the dev server’s HMR channel. It is also a stub in dev when timeline is off. Imports are safe to leave in component code. The ambient types ship in @oddsquad/vite-plugin-lit/client, the same declarations that cover the CSS queries.