This site uses React, Vue, and Svelte — all on the same page in some places. That's not an accident or a sign of chaos. It's Astro's islands architecture doing exactly what it's designed for.
The core idea: your page is static HTML by default. JavaScript only ships for the specific components that need it, and each component ("island") hydrates independently. The header? Zero JS. The interactive counter demo? Hydrated when it enters the viewport. The contact form? Hydrated immediately on load. You control each one.
## The same component in three frameworks
Here's a counter in React, Vue, and Svelte — the same behavior, three different styles:
**React:**
```tsx
// buttonReact.tsx
import { useEffect, useState } from "react"
export default function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
setTimeout(() => setCount((c) => c + 1), 1000)
}, [])
return (
```
React is explicit about state with hooks. Vue uses reactive refs with a template. Svelte is the most minimal — `let count = 0` is already reactive. All three compile to working interactive components.
## Hydration directives
This is where islands architecture gets practical. Without a `client:*` directive, any component renders to static HTML at build time and ships zero JavaScript:
```astro
---
import ReactCounter from '@components/react/buttonReact.tsx'
import VueCounter from '@components/vue/buttonVue.vue'
import SvelteCounter from '@components/svelte/buttonSvelte.svelte'
---
```
There's also `client:media` (hydrates when a media query matches) and `client:only` (skips SSR entirely, client-side only). I use `client:only` for components that read `localStorage` or `window` directly and would break during server rendering.
## Configuration
Three lines in `astro.config.mjs`:
```javascript
import { defineConfig } from "astro/config"
import react from "@astrojs/react"
import vue from "@astrojs/vue"
import svelte from "@astrojs/svelte"
export default defineConfig({
integrations: [react(), vue(), svelte()],
})
```
Astro handles bundling each framework separately so they don't interfere with each other. You get the right runtime for each island, nothing extra.
The thing I keep coming back to: before islands architecture, adding a single interactive component to a mostly-static page still meant shipping a full React runtime to every visitor. Now it means shipping exactly what that one component needs, and only when it's actually needed.