openElement's Ocean/Island model: server pre-renders content via DSD, client upgrades interactive components on demand.
The "ocean" is purely static content (layout, text, navigation). "Islands" are components that need client-side interactivity. The core insight: most of a page doesn't need JavaScript — only a few interactive spots do.
Declarative Shadow DOM lets server-rendered Web Components have their shadow root during HTML parsing. Users see content immediately — no JavaScript required.
<my-card>
<template shadowrootmode="open">
<style>:host { display: block; }</style>
<p>Content is visible before JavaScript loads.</p>
</template>
</my-card>| Layer | Type | Client JS | Best Fit |
|---|---|---|---|
| Layer 1 | DSD Static | None | Layout, navigation, article content |
| Layer 2 | DSD Interactive | Events only | Theme toggles, disclosure, tabs |
| Layer 3 | Pure Island | Full client logic | Charts, complex forms, WebSocket |
Declare islands via defineIsland() API with four hydration strategies:
import { defineIsland } from '@openelement/core';
export class MyChart extends DsdElement { /* ... */ }
// Load immediately (above-the-fold interactive elements)
defineIsland(MyChart, { strategy: 'load' });
// Defer until browser is idle (non-critical UI)
defineIsland(MyChart, { strategy: 'idle' });
// Load when entering viewport (lazy-loaded content)
defineIsland(MyChart, { strategy: 'visible' });
// Client-only render (no DSD, no SSR)
defineIsland(MyChart, { strategy: 'only' });| Strategy | Trigger | Recommended Use |
|---|---|---|
load | Module load | Above-fold: nav menus, search boxes |
idle | requestIdleCallback | Non-critical: footer widgets |
visible | IntersectionObserver | Lazy: image galleries, comments |
only | Client-only | Browser-specific: charts, maps |
Place components that need client-side behavior in the app/islands/ directory:
// app/islands/counter.ts
import { DsdElement, signal } from '@openelement/core';
export class Counter extends DsdElement {
#count = signal(0);
override render() {
return (
<div>
<button onClick={() => this.#count.value--}>-</button>
<span>{this.#count}</span>
<button onClick={() => this.#count.value++}>+</button>
</div>
);
}
}
customElements.define('my-counter', Counter);Usage in pages:
<my-counter></my-counter>The builder automatically scans app/islands/, generates a client entry, and injects it into the static HTML. Page HTML renders first; the browser upgrades components after loading the island entry.
| Aspect | SSR (Server-Side) | CSR (Client-Side) |
|---|---|---|
| Render output | DSD HTML string | Live DOM in shadow root |
| Signal subscriptions | Collected during render, serialized | Active — DOM updates on change |
| Event handlers | Serialized for hydration | Bound via addEventListener |
| effect() | Runs once, output captured | Runs continuously |
| ref | Silently skipped | Callback invoked |
customElements.define()to upgrade existing elements into real Custom Elements.