AvalonAvalon
GitHub

Lit

Using Lit web components as islands in Avalon.

Setup

Add 'lit' to the integrations array:

const plugins = await avalon({
	integrations: ['lit'],
});

Install Lit:

bun add lit

TypeScript configuration

Lit's decorators use the legacy/experimental decorator spec. Add experimentalDecorators to your tsconfig.json so TypeScript understands the decorator types correctly:

{
	"compilerOptions": {
		"experimentalDecorators": true
	}
}

Without this, you'll see type errors like "Unable to resolve signature of property decorator" on @state() and @property(). The setting only affects type-checking — at build time, Avalon's Lit integration uses Babel to compile decorators regardless of this flag.

Writing a Lit island

Lit islands use the .lit.ts extension so Avalon knows to use the Lit integration. Use Lit's decorator syntax for a clean, idiomatic API:

// src/islands/my-counter.lit.ts
import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { defineLitIsland } from '@useavalon/lit/island';

@customElement('my-counter')
export class MyCounter extends LitElement {
	static styles = css`
		button {
			padding: 8px 16px;
			cursor: pointer;
		}
	`;

	@state() count = 0;

	render() {
		return html` <button @click=${() => this.count++}>Count: ${this.count}</button> `;
	}
}

export default defineLitIsland(MyCounter);

The defineLitIsland wrapper is a zero-cost identity function — it returns the class unchanged at runtime but provides Preact-compatible types so TypeScript accepts <MyCounter /> in page files. See Type Compatibility for why this is needed.

Available decorators

Avalon supports all Lit decorators out of the box:

  • @customElement('tag-name') — registers the custom element
  • @property() — reactive property (reflected to attribute)
  • @state() — internal reactive state (not reflected)
  • @query('selector') — shorthand for this.renderRoot.querySelector()
  • @queryAll('selector') — shorthand for querySelectorAll()
  • @eventOptions({ ... }) — event listener options

How decorators work under the hood

Vite 8's default transformer (Oxc) doesn't support lowering TC39 standard decorators, and Lit's decorators use the legacy spec. Avalon's Lit integration automatically runs Babel with @babel/plugin-proposal-decorators (legacy mode) on any .lit.ts or .lit.js file that imports from lit/decorators. Non-decorator Lit files skip Babel entirely.

Using the island

Import the component and use it like any other island:

import MyCounter from '../islands/my-counter.lit.ts';

export default function Page() {
	return (
		<div>
			<MyCounter island={{ condition: 'on:interaction' }} />
		</div>
	);
}

SSR with Lit

Avalon uses @lit-labs/ssr to server-render Lit components. Ensure you have the SSR packages installed:

bun add @lit-labs/ssr @lit-labs/ssr-client @lit-labs/ssr-dom-shim

Declarative Shadow DOM

Lit's SSR output uses Declarative Shadow DOM, which is supported in all modern browsers. The client-side hydration attaches to the existing shadow root without re-rendering.

Properties vs attributes

Pass data to Lit islands using attributes (strings) or properties (any type via the .prop binding syntax in templates). When using Avalon's island prop, pass serializable values only.