Vue islands
Astro renders Vue components to HTML on the server and hydrates only the ones you mark with a client:* directive. Static Bloom’s Vue components work both ways: without a directive they ship as static HTML, and as islands they become interactive.
Comms
The panel above is a Vue island on this Starlight page: a switch, a slider and a select from @static-bloom/vue. Switch the flavor in the header and it follows.
-
Add Vue, the Astro renderer, the components and Tailwind.
Terminal window pnpm add @astrojs/vue vue @static-bloom/astro @static-bloom/vue @static-bloom/tailwindpnpm add -D tailwindcss @tailwindcss/vite -
Register the Vue renderer and the Tailwind Vite plugin next to the integration. Tailwind brings the theme, so the integration imports no CSS.
astro.config.mjs import { defineConfig } from 'astro/config'import vue from '@astrojs/vue'import tailwindcss from '@tailwindcss/vite'import staticBloom from '@static-bloom/astro'export default defineConfig({integrations: [vue(), staticBloom({ styles: false })],vite: { plugins: [tailwindcss()] },}) -
Create the global stylesheet and import it in your layout.
src/styles/global.css @import 'tailwindcss';@import '@static-bloom/tailwind';@import '@static-bloom/vue/tailwind.css';The last line makes Tailwind scan the components’ sources, so their classes are generated.
On a Starlight site, keep the Starlight stylesheet instead and add only vue() to the integrations.
Write an island
Section titled “Write an island”Each island is its own Vue app, so wrap its content in SbProvider. This is the panel above:
<script setup lang="ts">// Island demo for the Vue islands page, which shows this file's source verbatim.import { ref, useId } from 'vue'import { SbLabel, SbProvider, SbSelect, SbSelectContent, SbSelectItem, SbSelectTrigger, SbSelectValue, SbSlider, SbSwitch,} from '@static-bloom/vue'
const id = useId()const pushToTalk = ref(true)const volume = ref([70])const channel = ref('dispatch')const channels = [ { value: 'dispatch', label: 'Dispatch' }, { value: 'crew', label: 'Crew' }, { value: 'open', label: 'Open band' },]</script>
<template> <SbProvider> <div class="grid w-full max-w-sm gap-5 rounded-lg border border-line bg-panel p-5 shadow-sm"> <p class="m-0 font-display text-lg font-semibold text-fg">Comms</p> <div class="flex items-center justify-between gap-4"> <SbLabel :for="`${id}-ptt`">Push to talk</SbLabel> <SbSwitch :id="`${id}-ptt`" v-model="pushToTalk" /> </div> <div class="grid gap-3"> <div class="flex items-center justify-between gap-4"> <SbLabel as="span">Radio volume</SbLabel> <span class="font-mono text-xs text-fg-muted">{{ volume[0] }}%</span> </div> <!-- The thumb is the focusable control, so it carries the accessible name. --> <SbSlider v-model="volume" :max="100" :step="5" :thumb-labels="['Radio volume']" /> </div> <div class="grid gap-2"> <SbLabel :for="`${id}-channel`">Channel</SbLabel> <SbSelect v-model="channel"> <SbSelectTrigger :id="`${id}-channel`"> <SbSelectValue placeholder="Pick a channel" /> </SbSelectTrigger> <SbSelectContent> <SbSelectItem v-for="item in channels" :key="item.value" :value="item.value"> {{ item.label }} </SbSelectItem> </SbSelectContent> </SbSelect> </div> </div> </SbProvider></template>Then render it with a directive:
---import CommsPanel from '../components/CommsPanel.vue'---<CommsPanel client:visible />| Directive | Hydrates | Use for |
|---|---|---|
| none | never: static HTML | Components that only display |
client:visible |
when it scrolls into view | Most interactive components |
client:idle |
when the browser is idle | Below-the-fold widgets that should be ready soon |
client:load |
immediately | Controls the visitor uses first, above the fold |
Islands do not share Vue state. Pass data through props, or keep shared state outside Vue, such as the flavor runtime.
Flavors
Section titled “Flavors”The flavor runtime from the integration (or the Starlight plugin) owns <html>. Inside an island, useFlavor and SbFlavorSelect detect window.staticBloom, delegate to it, and listen to static-bloom:flavor-change, so every island and the page’s own selectors agree. The head script applies the flavor before islands hydrate, so nothing flashes.
To pin one island to a flavor, pass it to the provider: <SbProvider flavor="blackout"> wraps the content in a data-flavor scope.
Server rendering
Section titled “Server rendering”Static Bloom’s components are safe to render on the server: SbProvider hands Vue’s useId to Reka so ids match after hydration, and useFlavor reads nothing from window until the component mounts. Avoid client:only="vue" unless a component cannot render on the server; it removes the HTML until the script loads.