Skip to content
Blume is now publicly available.
Blume
Esc
navigateopen⌘Jpreview
On this page

Islands

Drop an interactive component into islands/ and use it in any MDX page — hydrated automatically, no per-page import.

Blume renders your docs as static HTML with zero JavaScript by default. When you need something interactive — a live demo, a chart, a playground — you add an island: a framework component that ships JS only for itself, only on the pages that use it.

The islands/ convention

Drop a component into an islands/ folder at your project root. Its filename becomes a component you can use in any .mdx page, with no import:

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Clicked {count}</button>;
}
Here's a live counter: <Counter />

The filename is the component name, so it must be a PascalCase identifier — letters, digits, and underscores only (Counter.tsx<Counter />). Lowercase filenames, names with dashes/dots/spaces (like Time-Picker.tsx), and two islands that resolve to the same name are skipped with a build warning.

Registering islands in components.ts

If you’d rather keep islands next to the rest of your components — or give them a different name than the file — register them with defineComponents. The islands group is exactly like the islands/ folder: each entry is available in every MDX page and hydrates (defaulting to client: "visible").

import { defineComponents } from "blume";
import Counter from "./widgets/Counter.tsx";

export default defineComponents({
  islands: {
    Counter, // <Counter /> in any MDX page, hydrated
  },
});

Reference the component by import or by a path string, and set a hydration mode per island with the descriptor form:

export default defineComponents({
  islands: {
    Chart: { component: "./widgets/Chart.tsx", client: "only" },
  },
});

Hydration

By default an island uses client:visible: it hydrates when the reader scrolls it into view, so a page full of islands still loads instantly. Opt into a different strategy with an export const client in the island file:

// Skip server rendering entirely — for components that touch the DOM/window.
export const client = "only";

export default function Chart() {
  /* ... */
}
client value Hydrates Use it for
"visible" (default) When scrolled into view Most islands
"load" Immediately on page load Above-the-fold, must-be-instant UI
"idle" When the main thread is idle Non-urgent interactivity
"only" Client only, never server-rendered Libraries that need window/document (charts, editors)

Frameworks

React works out of the box — Blume turns it on automatically the moment your project contains a .tsx/.jsx island.

The React Compiler is on by default whenever React is enabled, so your islands are auto-memoized — no hand-written useMemo/useCallback needed. It ships with Blume; there’s nothing to install. Opt out in blume.config.ts:

export default defineConfig({
  react: { compiler: false },
});

Vue and Svelte are supported too; install the matching Astro integration and Blume wires up the renderer when it sees a .vue or .svelte island:

# Vue
npm install @astrojs/vue vue

# Svelte
npm install @astrojs/svelte svelte
<script setup>
import { ref } from "vue";
const on = ref(false);
</script>

<template>
  <button @click="on = !on">{{ on ? "On" : "Off" }}</button>
</template>

Props you pass in MDX (<Counter start={5} />) are forwarded to the component, and children (<Counter>label</Counter>) arrive as the default slot.

Hooks

Islands hydrate on their own, so there’s no React context to thread project data through. Instead, blume/hooks reads a small snapshot the layout serializes into the page — no props to drill:

import { useBlume, usePage } from "blume/hooks";

export default function PageInfo() {
  const blume = useBlume();
  const page = usePage();
  if (!(blume && page)) {
    return null;
  }
  return (
    <p>
      You're reading <strong>{page.title}</strong> on {blume.config.title}.
    </p>
  );
}
Hook Returns
useBlume() { config, navigation } for the site, or null before mount
usePage() { route, title } for the current page, or null before mount
useSearch() { search, results, loading } — query the configured search provider
useAskAI() { ask, messages, loading, reset } — stream from the Ask AI endpoint

useBlume() and usePage() return null until the island mounts (so server and client render the same first frame) — guard for it. The snapshot is emitted only on pages that ship React, so a fully static site pays nothing.

On custom pages built with PageLayout, pass clientData so islands there can read it:

<PageLayout
  clientData={{ config: data.config, navigation: data.navigation, page: { route: "/", title: "Home" } }}
  {/* …other props… */}
/>

Was this page helpful?