Web Components Islands
Combine Islands with Web Components for style encapsulation and interoperability.
Why Web Components?
Web Components provide:
| Feature | Benefit |
|---|---|
| Shadow DOM | Style encapsulation |
| Custom Elements | Standard API |
| Declarative Shadow DOM | SSR support |
| Interoperability | Works anywhere |
Creating a Web Component Island
Server-Rendered HTML
The server renders an island with Web Component attributes:
For server-side rendering with MoonBit, see the MoonBit Tutorial.
<wc-counter
luna:wc-url="/static/wc-counter.js"
luna:wc-state="0"
luna:wc-trigger="load"
>
<template shadowrootmode="open">
<style>
:host { display: block; padding: 16px; }
button { background: blue; color: white; }
</style>
<button>Count: 0</button>
</template>
</wc-counter>
Client Side (TypeScript)
// wc-counter.ts
import { createSignal, hydrateWC } from '@luna_ui/luna';
interface CounterProps {
initial: number;
}
function Counter(props: CounterProps) {
const [count, setCount] = createSignal(props.initial);
return (
<>
<style>
{`:host { display: block; padding: 16px; }
button { background: blue; color: white; }`}
</style>
<button onClick={() => setCount(c => c + 1)}>
Count: {count()}
</button>
</>
);
}
// Register as Web Component
hydrateWC("wc-counter", Counter);
Declarative Shadow DOM
Luna uses Declarative Shadow DOM for SSR:
<my-component>
<template shadowrootmode="open">
<style>/* Scoped styles */</style>
<!-- Shadow DOM content -->
</template>
</my-component>
Benefits:
Styles apply immediately (no FOUC)
No JavaScript needed for initial render
Content visible before hydration
Style Encapsulation
Styles inside Shadow DOM are scoped:
/* These only affect the component */
:host {
display: block;
border: 1px solid #ccc;
}
button {
/* Won't affect buttons outside */
background: blue;
}
/* Style from outside */
::slotted(*) {
/* Styles for slotted content */
}
Global Styles
To use global styles, adopt stylesheets:
// In your component
const globalSheet = new CSSStyleSheet();
globalSheet.replaceSync(document.querySelector('style#global').textContent);
class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.adoptedStyleSheets = [globalSheet];
}
}
Slots
Pass content into Web Components:
HTML Structure
<wc-card>
<p>Card content goes in default slot</p>
<footer slot="footer">Footer content</footer>
</wc-card>
WC vs Regular Islands
| Aspect | Regular Island | WC Island |
|---|---|---|
| Styles | Global CSS | Scoped (Shadow DOM) |
| Element | <div> | Custom element |
| SSR | innerHTML | Declarative Shadow DOM |
| Slots | Not supported | Supported |
| Outside styling | Easy | Requires CSS parts |
When to Use Web Components
Use WC Islands for:
Components needing style isolation
Reusable across different projects
Components with slots
Design system components
Use Regular Islands for:
Simple interactive widgets
Components that need global styles
Quick prototyping
CSS Parts
Expose style hooks for outside customization:
// In your component
<button part="button">Click me</button>
/* From outside */
wc-counter::part(button) {
background: red; /* Override internal style */
}
Events
Dispatch custom events from Web Components:
function Counter() {
const host = useHost(); // Get the custom element
const handleClick = () => {
setCount(c => c + 1);
// Dispatch custom event
host.dispatchEvent(new CustomEvent('count-changed', {
detail: { count: count() },
bubbles: true,
}));
};
return <button onClick={handleClick}>Count: {count()}</button>;
}
<wc-counter oncount-changed="handleCountChange(event)"></wc-counter>
Common Patterns
Card Component
<wc-card luna:wc-url="/static/wc-card.js" luna:wc-trigger="load">
<template shadowrootmode="open">
<style>
:host {
display: block;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 16px;
}
::slotted(h2) {
margin-top: 0;
}
</style>
<slot></slot>
</template>
</wc-card>
Modal Component
<wc-modal luna:wc-url="/static/wc-modal.js" luna:wc-trigger="none">
<template shadowrootmode="open">
<style>
:host {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.5);
}
.modal {
background: white;
padding: 24px;
border-radius: 8px;
}
</style>
<div class="modal">
<slot></slot>
</div>
</template>
</wc-modal>
Tab Component
<wc-tabs luna:wc-url="/static/wc-tabs.js" luna:wc-trigger="load">
<template shadowrootmode="open">
<style>
:host { display: block; }
.tabs { display: flex; border-bottom: 1px solid #ccc; }
.tab { padding: 8px 16px; cursor: pointer; }
.tab.active { border-bottom: 2px solid blue; }
</style>
<!-- Tab content -->
</template>
</wc-tabs>
Try It
Design a Web Component island for a notification toast:
Solution
HTML Output:
<wc-toast
luna:wc-url="/static/wc-toast.js"
luna:wc-state='{"message":"Hello!","type":"success"}'
luna:wc-trigger="load"
>
<template shadowrootmode="open">
<style>
:host {
position: fixed;
bottom: 24px;
right: 24px;
padding: 16px 24px;
border-radius: 8px;
color: white;
animation: slide-in 0.3s ease;
}
:host(.success) { background: #22c55e; }
:host(.error) { background: #ef4444; }
:host(.info) { background: #3b82f6; }
button {
background: none;
border: none;
color: white;
cursor: pointer;
margin-left: 16px;
}
@keyframes slide-in {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
</style>
<span>Hello!</span>
<button>×</button>
</template>
</wc-toast>
Client (TypeScript):
interface ToastProps {
message: string;
type: "success" | "error" | "info";
}
function Toast(props: ToastProps) {
const [visible, setVisible] = createSignal(true);
const host = useHost();
onMount(() => {
host.classList.add(props.type);
// Auto-dismiss after 5 seconds
const timeout = setTimeout(() => setVisible(false), 5000);
onCleanup(() => clearTimeout(timeout));
});
const dismiss = () => {
setVisible(false);
host.remove();
};
return (
<Show when={visible()}>
<span>{props.message}</span>
<button onClick={dismiss}>×</button>
</Show>
);
}
hydrateWC("wc-toast", Toast);
Summary
You've completed the Luna tutorial! You now know:
Signals for reactive state
Effects for side effects
Memos for computed values
Control Flow for conditional/list rendering
Lifecycle for mount/cleanup
Islands for partial hydration
Web Components for encapsulation
Next Steps
Read the JavaScript API Reference
Read the MoonBit Tutorial for server-side rendering
Explore the Examples