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 forthis.renderRoot.querySelector()@queryAll('selector')— shorthand forquerySelectorAll()@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.