# Runtime API

> The browser helpers exported from @oddsquad/vite-plugin-lit/css.js, and the virtual:lit-plugin/timeline module.

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.

## urlSheet()

```ts
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.

```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.

Fetch failures are silent

A wrong path or a 404 leaves the sheet empty and logs nothing. If a shared sheet appears to do nothing, check the network panel for the CSS request before you suspect the HMR wiring.

### UrlSheet

```ts
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()`.

## devCacheBust()

```ts
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.

```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>`;
  }
}
```

Call it at module scope

Read the busted href once, outside `render()`. Inside a render, every render would produce a new href and refetch the stylesheet.

## addTimelineLayer()

```ts
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.

## addTimelineEvent()

```ts
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'`.

```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.

## Related

[Share a sheet with urlSheet()](/guides/stylesheets/url-sheet/)The wiring in context, with the accept-line caveat.

[Custom layers](/guides/devtools/custom-layers/)Putting your own events on the timeline.
