Skip to content
Astro as components 4 min read

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.

DirectiveWhen it hydratesBest for
client:loadImmediately on page loadAbove-the-fold, high-priority UI
client:idleWhen the main thread is idleLow-priority UI that can wait
client:visibleWhen the element scrolls into viewBelow-the-fold widgets, heavy components
client:media={query}When a CSS media query matchesMobile-only or desktop-only UI
client:only={framework}Skips SSR; renders only in browserComponents 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:only requires 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 .astro file 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:visible for 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 .astro components as the connective tissue when combining multiple frameworks on one page.
  • Reserve client:load for genuinely critical, above-the-fold interactivity.
Last updated June 14, 2026
Was this helpful?