Server/Packages/Islands/Client directives
@sigx/ssr-islands · Stable

Client directives#

A client:* directive on a component tells the islands plugin to hydrate it — and when. Components without a client:* directive render to static HTML and ship no client JavaScript.

Enable the attributes with a one-time type import — see Installation:

TSX
import '@sigx/ssr-islands/jsx';

The strategies#

TSX
<Counter client:load />                       {/* hydrate immediately */}
<Sidebar client:idle />                       {/* hydrate when the browser is idle */}
<Chart client:visible />                      {/* hydrate when scrolled into view */}
<Menu client:media="(min-width: 768px)" />    {/* hydrate when the query matches */}
<Search client:interaction />                 {/* hydrate on the first interaction */}
<LiveFeed client:only />                       {/* skip SSR, mount fresh on the client */}

client:load#

Hydrates as soon as hydrateIslands() runs. Use for above-the-fold interactivity that must be live immediately (primary nav, a hero CTA).

client:idle#

Defers hydration to browser idle time (requestIdleCallback, with a timeout fallback). Good for secondary widgets that should not compete with first paint.

client:visible#

Hydrates when the element scrolls into the viewport, via IntersectionObserver. Ideal for below-the-fold islands — charts, comment threads, carousels — so their JS only loads when the user reaches them. Pairs naturally with lazy registration for per-island code splitting.

client:media#

Hydrates when a CSS media query matches. Pass the query as the value:

TSX
<MobileMenu client:media="(max-width: 767px)" />
<DesktopNav client:media="(min-width: 768px)" />

The component stays static until (and unless) the query matches — so a desktop visitor never pays for the mobile menu's JavaScript, and vice versa.

client:interaction#

Hydrates on the first interaction with the element — the first pointerdown, keydown, touchstart, or focusin. It fires once, and the triggering event is not replayed, so the island wakes and the user's next action lands on the live component. Ideal for controls that are visible but only matter once touched — a search box, a "reply" affordance, a menu that opens on click — where you want the static HTML immediately and the JavaScript only when the user commits.

client:only#

Genuinely skips server rendering. The component is not run on the server: its setup/render never execute, and the server emits an empty <div data-boundary> placeholder in its place — no server HTML for the component itself. The island still gets a boundary record in __SIGX_BOUNDARIES__ (so the client knows to mount it), but with no captured signal state, since nothing ran server-side. On the client it mounts fresh.

Use it for components that cannot render on the server — ones that read window, localStorage, or other browser-only globals during setup.

Contrast with client:load, which does render on the server (you get the SSR HTML immediately) and then hydrates that existing DOM, resuming from the captured signal state. Reach for client:only only when server rendering is impossible or unwanted; otherwise a server-rendered strategy gives a faster first paint.

The strategy type#

The six strategies are the HydrationStrategy union, and the directives are the ClientDirectives interface that augments component attributes:

TypeScript
type HydrationStrategy = 'load' | 'idle' | 'visible' | 'media' | 'interaction' | 'only';

interface ClientDirectives {
    'client:load'?: boolean;
    'client:idle'?: boolean;
    'client:visible'?: boolean;
    'client:media'?: string;   // the media query
    'client:interaction'?: boolean;
    'client:only'?: boolean;
}

Directives vs. boundary axes#

These directives are the islands pack's friendly surface over the two boundary axes. The two vocabularies don't line up one-to-one, and it's worth knowing why: client:only is a flush concern — it maps to { flush: 'skip', hydrate: 'load' } — while the boundary model's hydrate: 'never' (server HTML, no client component) has no directive of its own. So don't read the directive list and the hydrate axis as the same enum.

Cleaning up on SPA navigation#

Deferred strategies register observers and listeners (IntersectionObserver, matchMedia) for islands that have not triggered yet. When you navigate away in an SPA before they fire, call cleanupPendingHydrations() to tear those down and avoid leaks:

TypeScript
import { cleanupPendingHydrations } from '@sigx/ssr-islands';

router.beforeEach(() => {
    cleanupPendingHydrations();
});

Next steps#