Skip to content

Choose how to deliver CSS

Pick the CSS delivery that fits: one shared sheet for utility classes, or per-component styles for rules one component owns. The table decides it; the linked pages do it.

A shared stylesheet is one CSSStyleSheet object adopted by many shadow roots. The top three rows deliver one; the bottom two give each component its own copy.

Shape Use when What an edit does Page
?css-sheet many components share a sheet and you want it cached as its own .css asset restyles every adopter in place, no re-render ?css-sheet
urlSheet() the same, but you need a custom fetch or code that runs without this plugin restyles every adopter in place, no re-render urlSheet()
?raw into one shared module many components share a sheet and first paint must not flash restyles every adopter in place, no re-render Component styles
?hmr-url <link> rules one component owns, kept as a real .css asset re-renders that component Component styles
?raw or ?inline + unsafeCSS rules one component owns, shipped in the JS chunk re-renders that component Component styles

A re-render is cheap for one component’s own rules and expensive for a layer every component adopts. CSS delivery has the measurements behind that.

A generated utility sheet is the case the top rows exist for. Every component adopts the same object, so a regenerated file restyles the whole app without re-rendering anything.

Choose by where the bytes should live. ?css-sheet and urlSheet() keep the CSS as a separate, content-hashed .css asset, at the cost of FOUC: a brief flash of unstyled content while the stylesheet is still loading. The inline shared module ships the bytes in a JS chunk. There is no fetch and no flash, but the CSS is not cacheable on its own.

Compose a shared sheet with component styles

Section titled “Compose a shared sheet with component styles”

static styles takes an array, so the two shapes stack. Put the shared sheet first and the component’s own css block after it, and the later rules win at equal specificity.

src/hmr-utility-btn.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import utilitySheet from './hmr-utility-sheet.js';
/**
* Button styled via utility classes from a shared stylesheet — simulates a
* Tailwind/UnoCSS workflow. All utility consumers update in place when the
* generated CSS changes (e.g. a new theme color), without re-rendering.
*
* Approach benefits/problems: see `hmr-utility-sheet` (fetched shared sheet).
*/
@customElement('hmr-utility-btn')
export class HmrUtilityBtn extends LitElement {
static override styles = [utilitySheet];
@property()
variant: 'blue' | 'red' = 'blue';
private renders = 0;
override render() {
const bgClass = this.variant === 'blue' ? 'bg-blue' : 'bg-red';
return html`
<button
class="${bgClass} text-white font-bold py-2 px-4 rounded border-none cursor-pointer"
>
<slot></slot>
</button>
<span class="font-mono text-xs" id="badge">renders: 0</span>
`;
}
override updated() {
this.renders++;
this.setAttribute('data-renders', String(this.renders));
const badge = this.renderRoot.querySelector('#badge');
if (badge !== null) {
badge.textContent = `renders: ${this.renders}`;
}
}
}