UI Framework Components
Astro is framework-agnostic by design. You can author components in React, Vue, Svelte, Solid, Preact, AlpineJS, or Lit and mix them freely with native .astro components in the same project — even on the same page. Astro renders every component to static HTML at build time by default, so a page ships zero JavaScript unless you explicitly opt a component into client-side hydration. This is the heart of Astro’s islands architecture: interactive UI lives in small, isolated islands while the rest of the page stays pure HTML.
Adding a framework integration
Before you can use a UI framework, install its official Astro integration. The astro add command installs the npm packages, updates astro.config.mjs, and wires up the necessary peer dependencies in one step.
npx astro add react
npx astro add vue
npx astro add svelte
npx astro add solid
npx astro add preact
Each command edits your config to register the integration:
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import vue from "@astrojs/vue";
export default defineConfig({
integrations: [react(), vue()],
});
You can register multiple integrations at once. Astro detects each component’s framework by its file extension (.jsx/.tsx, .vue, .svelte) and renders it with the matching renderer.
Authoring a framework component
Framework components are written exactly as you would in a standalone app — Astro imposes no special syntax. Here is an ordinary React counter:
// src/components/Counter.tsx
import { useState } from "react";
export default function Counter({ start = 0 }: { start?: number }) {
const [count, setCount] = useState(start);
return (
<button onClick={() => setCount((c) => c + 1)}>
Count: {count}
</button>
);
}
A Svelte equivalent looks just as native:
<!-- src/components/Counter.svelte -->
<script lang="ts">
export let start = 0;
let count = start;
</script>
<button on:click={() => (count += 1)}>Count: {count}</button>
Using framework components in .astro files
Import a framework component into the frontmatter (the --- script fence) of an .astro file and render it like any other component. Without a client:* directive, it renders to static HTML and ships no JavaScript.
---
// src/pages/index.astro
import Counter from "../components/Counter.tsx";
import Card from "../components/Card.astro";
---
<Card>
<!-- Static HTML only: zero JS shipped -->
<Counter start={5} />
<!-- Interactive: hydrates in the browser -->
<Counter start={5} client:load />
</Card>
Hydration with client directives
The client:* directives tell Astro when and whether to ship and hydrate a component’s JavaScript. Choosing the right one is the main lever you have over page performance.
| Directive | When it hydrates | Best for |
|---|---|---|
client:load | Immediately on page load | Above-the-fold, high-priority UI |
client:idle | When the main thread is idle | Low-priority UI that can wait |
client:visible | When the element scrolls into view | Below-the-fold widgets, heavy components |
client:media={query} | When a CSS media query matches | Mobile-only or desktop-only UI |
client:only={framework} | Skips SSR; renders only in browser | Components that depend on browser-only APIs |
---
import Chart from "../components/Chart.jsx";
import Menu from "../components/Menu.vue";
---
<Menu client:visible />
<Chart client:only="react" />
Note
client:onlyrequires you to name the framework as a string ("react","vue","svelte","solid","preact") because Astro never server-renders the component and therefore cannot infer the renderer from the output.
Passing props and children
Props pass from .astro to framework components like normal attributes. They must be serializable (strings, numbers, booleans, plain objects, arrays) because they are sent across the server-to-client boundary as JSON. Functions, class instances, and Maps cannot cross that boundary.
You can also pass children. In .astro, the default slot maps to the framework’s children mechanism — children in React/Preact/Solid, the default <slot> in Vue/Svelte. Named slots are supported via the slot="name" attribute.
---
import Modal from "../components/Modal.tsx";
---
<Modal client:load title="Welcome">
<p>This paragraph is passed as children.</p>
<span slot="footer">Footer content</span>
</Modal>
Mixing frameworks and nesting islands
Astro lets a single page contain React, Vue, and Svelte components side by side. You can even nest framework components inside one another — but a hydrated island can only contain components from its own framework. To embed a different framework inside, pass it through an .astro component as a slot, since .astro is the universal glue.
Warning You cannot import a Vue component directly inside a React island. Instead, render both from an
.astrofile and pass one into the other via a slot.
Best Practices
- Prefer no directive (static rendering) unless a component genuinely needs interactivity — every island you add is JS the user must download.
- Reach for
client:visiblefor anything below the fold to defer its cost until it is actually seen. - Keep islands small and focused; hydrate a tiny interactive widget rather than a whole page section.
- Pass only serializable props across the island boundary, and quote framework names in
client:only. - Use
.astrocomponents as the connective tissue when combining multiple frameworks on one page. - Reserve
client:loadfor genuinely critical, above-the-fold interactivity.