AvalonAvalon
GitHub

Hydration Strategies

Advanced hydration directives and custom triggers for fine-grained control.

Overview

Avalon ships with four built-in conditions (on:client, on:visible, on:interaction, on:idle) and a media: condition covered in Islands Architecture. This page covers the custom directives that give you fine-grained control beyond those defaults.

Custom directives

Custom directives follow the on:<name> pattern and accept an optional argument via conditionArg. They are registered automatically — no setup needed.

on:delay

Hydrates after a timeout.

// Hydrate after 3 seconds (default: 1000ms)
<Widget island={{ condition: 'on:delay', conditionArg: '3000' }} />

Use when: You want to defer hydration by a fixed duration — e.g. a tooltip that isn't needed in the first few seconds.

on:scroll

Hydrates when the page scrolls past a pixel threshold.

// Hydrate when user scrolls past 500px
<LazySection island={{ condition: 'on:scroll', conditionArg: '500' }} />

Use when: The component should activate based on scroll depth rather than viewport intersection — e.g. a "back to top" button or a progress bar.

on:event

Hydrates when a custom DOM event fires on document.

// Hydrate when a custom event fires
<Dashboard island={{ condition: 'on:event', conditionArg: 'data:loaded' }} />

Use when: Hydration depends on an external signal — data finishing a fetch, a third-party script loading, or an app-level readiness event.

on:match

Hydrates when a media query matches. Similar to the built-in media: condition but uses the custom directive system and accepts the query as conditionArg.

// Hydrate when viewport is at least 1024px
<Sidebar island={{ condition: 'on:match', conditionArg: '(min-width: 1024px)' }} />

Registering your own directive

Register custom directives in your server/renderer.ts before the export:

import { registerHydrationDirective } from '@useavalon/avalon';

registerHydrationDirective('on:countdown', {
  name: 'on:countdown',
  script: (el, hydrate, arg) => {
    const seconds = parseInt(arg || '5', 10);
    let remaining = seconds;
    const tick = setInterval(() => {
      remaining--;
      if (remaining <= 0) {
        clearInterval(tick);
        hydrate();
      }
    }, 1000);
  },
});

export { default } from 'virtual:avalon/renderer';

Then use it on any island:

<MyComponent island={{ condition: 'on:countdown', conditionArg: '5' }} />

The directive's script function runs on the client. It receives:

ParameterDescription
elThe island's DOM element
hydrateCallback — call once to trigger hydration
argThe conditionArg string (or undefined)

Performance tips

  • Start with on:interaction or on:visible for most islands. They cover the majority of use cases with zero upfront JS cost.
  • Use on:delay to stagger hydration of multiple non-critical islands so they don't all compete for the main thread at once.
  • Prefer on:event over polling when hydration depends on async data — it avoids unnecessary timer overhead.
  • Avoid on:client unless the component truly needs to be interactive before any user action. Every on:client island adds to the critical path.
  • Combine on:scroll with a high threshold for components that only matter deep in the page — this is lighter than on:visible with a large root margin.