Islands
AbsoluteJS islands let one SSR-first page host interactive components from React, Vue, Svelte, and Angular without turning the whole page into a client app. Page handlers stay pure. AbsoluteJS owns the SSR markers, manifest wiring, bootstrap, hydration, and cross-framework runtime.
#Overview
Use islands when most of a page should remain server-rendered HTML, but a few areas need interactivity. A host page can stay in its own framework while selectively embedding islands from other frameworks.
zustand/vanilla underneath and exposes framework-specific selectors on top.#Registry
Define a registry once with direct component imports. This is the source of truth for SSR lookup and for the optional typed wrappers created with createTypedIsland(registry).
import { defineIslandRegistry } from '@absolutejs/absolute/islands';
import { createTypedIsland as createTypedAngularIsland } from '@absolutejs/absolute/angular';
import { createTypedIsland as createTypedReactIsland } from '@absolutejs/absolute/react';
import { createTypedIsland as createTypedSvelteIsland } from '@absolutejs/absolute/svelte';
import { createTypedIsland as createTypedVueIsland } from '@absolutejs/absolute/vue';
import { AngularCounter } from '../angular/components/AngularCounter';
import { ReactCounter } from '../react/components/ReactCounter';
import SvelteCounter from '../svelte/components/SvelteCounter.svelte';
import { VueCounter } from '../vue/components/VueCounter';
export const islandRegistry = defineIslandRegistry({
angular: { AngularCounter },
react: { ReactCounter },
svelte: { SvelteCounter },
vue: { VueCounter }
});
export const TypedAngularIsland = createTypedAngularIsland(islandRegistry);
export const TypedReactIsland = createTypedReactIsland(islandRegistry);
export const TypedSvelteIsland = createTypedSvelteIsland(islandRegistry);
export const TypedVueIsland = createTypedVueIsland(islandRegistry);The registry accepts real component values directly. No source strings, prop builders, or generated binding files are required in app code.
#Loose vs Typed
Each framework exports a built-in Island for loose authoring and a createTypedIsland(registry) helper for registry-driven typing. The loose primitive is useful when you want dynamic or ad hoc rendering. The typed wrapper is the strongest option when you want compile-time enforcement of the full framework → component → props chain.
import { Island } from '@absolutejs/absolute/react';
export const DashboardPage = () => (
<main>
<Island
framework="vue"
component="VueCounter"
hydrate="visible"
props={{
initialCount: 0,
label: 'Loose deferred Vue island'
}}
/>
</main>
);import { TypedReactIsland } from '../../islands/registry';
export const DashboardPage = () => (
<main>
<TypedReactIsland
framework="react"
component="ReactCounter"
hydrate="load"
props={{
initialCount: 0,
label: 'React island'
}}
/>
</main>
);// Loose Island accepts any framework/component string pair.
<Island
framework="vue"
component="VueCounter"
props={{ initialCount: 0, label: 'Loose island' }}
/>
// Typed islands are driven by the registry.
<TypedReactIsland
framework="react"
component="ReactCounter"
props={{ initialCount: 0, label: 'Typed island' }}
/>
// createTypedIsland(registry) narrows all three parts together:
// framework -> component -> props
//
// These fail at compile time:
// - wrong framework/component combinations
// - missing required props
// - extra props not accepted by the component#Island Stores
Shared state is not passed through island props. Instead, define a real store with createIslandStore(...) and import that same store into any island component that should share it. This follows the same store-first mental model you would use with Zustand itself.
import { createIslandStore } from '@absolutejs/absolute/islands';
export const counterIslandStore = createIslandStore(
'counter',
{
sharedCount: 0
},
(set) => ({
incrementShared: () =>
set((state) => ({
sharedCount: Number(state.sharedCount ?? 0) + 1
})),
resetShared: () => set({ sharedCount: 0 })
})
);// Import the same island store module from any island component.
// That is what makes the state shared across frameworks.
<Island
framework="react"
component="ReactCounter"
props={{ initialCount: 0, label: 'React island' }}
/>
<Island
framework="vue"
component="VueCounter"
props={{ initialCount: 0, label: 'Vue island' }}
/>AbsoluteJS uses zustand/vanilla under the hood.
The model is store-first, not key-first:
- define state and actions once with createIslandStore(...)
- import the same store into any island component
- select state and actions with framework-specific primitives
Serializable state is snapshotted for SSR.
Actions are recreated from your store definition on the client.#Hydration Modes
Hydration is per island, not page-wide.
<Island framework="react" component="Hero" hydrate="load" props={{ ... }} />
<Island framework="vue" component="AnalyticsPanel" hydrate="idle" props={{ ... }} />
<Island framework="svelte" component="PricingTable" hydrate="visible" props={{ ... }} />
<Island framework="react" component="StaticBadge" hydrate="none" props={{ ... }} />#HTML & HTMX
HTML and HTMX hosts use the platform-native <absolute-island> custom element instead of a component import. AbsoluteJS lowers it into SSR island markup and wires the runtime automatically.
<absolute-island
framework="react"
component="ReactCounter"
hydrate="load"
props='{"initialCount":0,"label":"React island"}'
></absolute-island>Because HTML attributes are strings, HTML and HTMX do not get the same prop-level inference as createTypedIsland(registry)on TS-based component hosts. They still use the same runtime, SSR, hydration modes, and island stores.