Type something to search...
What is the difference between Astro components and framework components?

What is the difference between Astro components and framework components?

The difference between Astro components and framework components is where they run. An Astro component (.astro) renders to HTML on the server, at build time or on request, and ships no JavaScript runtime to the browser; any interactivity has to come from a plain <script> tag. A framework component (React, Vue, Svelte, Solid, Preact and others) also renders to HTML on the server by default, but you can add a client:* directive to send its JavaScript to the browser and hydrate it into an interactive island with state, events and re-rendering. Use Astro components for layout and content, and framework components only for the parts of a page that need client-side state.

Both kinds of component can live side by side in src/components/, and from a page they look almost identical: you import them and write them as tags. The differences show up in what they can do, what they cost and how they pass data. In this article you'll see a side-by-side comparison, the same counter built both ways, what the browser actually receives, how props and slots differ across the boundary, which nesting is allowed, and a practical guide to choosing.

Side-by-Side Comparison

Astro componentFramework component
File type.astro.jsx, .tsx, .vue, .svelte and so on
SetupBuilt inNeeds an integration, such as @astrojs/react
Where it rendersServer onlyServer by default, plus the browser when hydrated
JavaScript sent by defaultNoneNone
Can be interactiveOnly through <script> tagsYes, with a client:* directive
State and re-renderingNo, renders onceYes, after hydration
PropsAny JavaScript value, including functionsMust be serialisable when hydrated
Children<slot /> and named slotschildren and props (React), <slot> (Vue, Svelte)
Can import the other kindYes, can import framework componentsNo, cannot import .astro files
Server-only code (secrets, database)Safe in the component scriptCode may be bundled for the browser when hydrated

The key row is "JavaScript sent by default". Neither sends any JavaScript unless you ask. A framework component without a directive renders to static HTML, just like an Astro component. The difference is that a framework component can be hydrated, and an Astro component can't. Why that default matters for performance is covered in how Astro ships zero JavaScript by default.

The Same Counter, Built Both Ways

A counter shows the difference clearly. First, as a React component:

// src/components/Counter.tsx
import { useState } from "react";

interface Props {
  initial?: number;
  label: string;
}

export default function Counter({ initial = 0, label }: Props) {
  const [count, setCount] = useState(initial);
  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      {label}: {count}
    </button>
  );
}

The React integration is added with npx astro add react. Now the same thing as an Astro component, with a script for the behaviour:

---
// src/components/AstroCounter.astro
interface Props {
  label: string;
}
const { label } = Astro.props;
---
<button type="button" class="astro-counter" data-label={label}>
  {label}: <span>0</span>
</button>

<script>
  document.querySelectorAll<HTMLButtonElement>(".astro-counter").forEach((button) => {
    const output = button.querySelector("span")!;
    let count = 0;
    button.addEventListener("click", () => {
      count += 1;
      output.textContent = String(count);
    });
  });
</script>

Using both on a page:

---
// src/pages/index.astro
import Counter from "../components/Counter.tsx";
import AstroCounter from "../components/AstroCounter.astro";
---
<Counter label="Static" />
<Counter label="Interactive" initial={5} client:visible />
<AstroCounter label="Astro" />
<AstroCounter label="Astro two" />

All four buttons render as HTML. Only three of them work when clicked: the first React counter has no directive, so it's plain HTML with no event handler attached.

What the Browser Receives

I built that page and looked at the output. The differences are instructive.

The static React counter became ordinary HTML:

<button type="button">Static<!-- -->: <!-- -->0</button>

The hydrated React counter was wrapped in an <astro-island> custom element that records where its code lives and what props to hydrate with:

<astro-island uid="2i2Yup" prefix="r5" component-url="/_astro/Counter.Dao5TgHo.js" component-export="default" renderer-url="/_astro/client.CzWaolnw.js" props="{&quot;label&quot;:[0,&quot;Interactive&quot;],&quot;initial&quot;:[0,5]}" ssr client="visible" ...><button type="button">Interactive<!-- -->: <!-- -->5</button><!--astro:end--></astro-island>

Along with it came a small inline script for the island loader and the client:visible strategy, plus the React runtime as a separate file. In this build, the React client chunk was about 213 kB uncompressed (about 66 kB gzipped), while the counter's own chunk was 261 bytes. That runtime is downloaded once per page however many React islands you use, but it's the main cost of a framework island.

The Astro counter rendered as HTML with one small script. Even though the component appeared twice, the script was included only once, because Astro processes and deduplicates <script> tags:

<button type="button" class="astro-counter" data-label="Astro">Astro: <span>0</span></button><script type="module">document.querySelectorAll(`.astro-counter`).forEach(e=>{let t=e.querySelector(`span`),n=0;e.addEventListener(`click`,()=>{n+=1,t.textContent=String(n)})});</script><button type="button" class="astro-counter" data-label="Astro two">Astro two: <span>0</span></button>

The script was small enough that Astro inlined it into the page. For a counter, the Astro version costs a few hundred bytes and the React version tens of kilobytes. For a complex, stateful UI, the framework's model pays for itself; for a toggle or a counter, it usually doesn't.

Hydration Directives Only Work on Framework Components

The client:* directives control when an island's JavaScript loads:

  • client:load: as soon as the page loads, for UI that must be interactive immediately.
  • client:idle: once the browser is idle, for lower-priority UI.
  • client:visible: when the component scrolls into view.
  • client:media: when a CSS media query matches, such as client:media="(max-width: 50em)".
  • client:only: skip server rendering and render only in the browser, naming the framework, such as client:only="react".

Putting a directive on an Astro component does nothing useful. When I tried <AstroCounter label="x" client:load />, the build still succeeded but logged:

You are attempting to render <AstroCounter client:load />, but AstroCounter is an Astro component. Astro components do not render in the client and should not have a hydration directive. Please use a framework component for client rendering.

The directives are covered one by one in Astro client directives explained.

Props Across the Boundary

Between Astro components, a prop can be any JavaScript value, because everything happens in one server process. Hydrated framework components are different: their props must be written into the HTML (you can see them in the props attribute above) and rebuilt in the browser. Astro's documentation lists the supported types as plain objects, number, string, Array, Map, Set, RegExp, Date, BigInt, URL, Uint8Array, Uint16Array, Uint32Array and Infinity.

Functions are the notable gap. You can't pass an onSave callback from an Astro page to a hydrated React component and expect it to run in the browser. Keep event handlers inside the framework component, or communicate between islands through shared state, as described in how to share state between islands.

Also remember that props of a hydrated component are visible to anyone who views the page source. Don't pass secrets or whole database records when the island only needs a couple of fields.

Children and Slots Across the Boundary

From an .astro file you can pass children to a framework component. How the component receives them depends on the framework:

  • React, Preact and Solid: the default slot arrives as children, and each named slot arrives as a prop, with kebab-case names converted to camelCase.
  • Vue and Svelte: they use their own <slot> elements, and slot names keep their original form.

Here's a React sidebar with named slots:

// src/components/Sidebar.tsx
import type { ReactNode } from "react";

interface Props {
  title?: ReactNode;
  socialLinks?: ReactNode;
  children?: ReactNode;
}

export default function Sidebar({ title, socialLinks, children }: Props) {
  return (
    <aside>
      <header>{title}</header>
      <div>{children}</div>
      <footer>{socialLinks}</footer>
    </aside>
  );
}
<Sidebar>
  <h2 slot="title">Menu</h2>
  <p>Sidebar body.</p>
  <ul slot="social-links"><li>GitHub</li></ul>
</Sidebar>

The social-links slot became the socialLinks prop, and the output was:

<aside><header><h2>Menu</h2></header><div><p>Sidebar body.</p></div><footer><ul><li>GitHub</li></ul></footer></aside>

Slots in Astro components are covered in what slots are and how to use named slots.

Nesting Rules

The rules about which component can contain which are strict, and they explain a lot of error messages:

  1. Astro components can import framework components, from any number of frameworks on the same page.
  2. Framework components cannot import Astro components. A .tsx or .svelte file must be valid code for its own framework, and .astro files aren't.
  3. You can still put Astro output inside a framework component, by passing it as a child from an .astro file. The Astro component renders on the server and its HTML is handed to the island as slot content.
  4. A framework file can't mix frameworks. Combine React and Vue in an .astro file, not inside one of them.

Rule 3 looks like this:

---
// src/pages/index.astro
import Sidebar from "../components/Sidebar.tsx";
import Callout from "../components/Callout.astro";
---
<Sidebar client:idle>
  <Callout slot="title">Rendered by Astro</Callout>
  <p>Hydrated body.</p>
</Sidebar>

In the build, the Astro component's HTML was placed inside the island in an <astro-slot> element:

<aside><header><astro-slot name="title"><div class="callout callout-note">Rendered by Astro</div></astro-slot></header>...

The callout is static HTML. The React component can move it around but can't change what's inside it, because the Astro component never runs in the browser.

Which Should You Use?

Start with an Astro component and only switch when you need something it can't do.

Choose an Astro component for:

  • Layouts, headers, footers and page sections: anything that's mostly markup.
  • Content rendering: blog posts, cards, lists, tables and navigation.
  • Server-side data: components that fetch from an API or database, read environment secrets or query content collections.
  • Light interactivity: menus, toggles, copy buttons and tabs that a short <script> can handle. See interactivity without a framework.

Choose a framework component for:

  • Complex state: forms with validation, filters that update a list, multi-step flows.
  • Frequent re-rendering: live search, charts that update, drag-and-drop.
  • Existing component libraries: when you already have React or Vue components, or need a library built for one framework.
  • Shared code with another app: a design system that's also used in a separate single-page app.

A common, healthy pattern is an Astro page made of Astro components, with one or two framework islands for the genuinely interactive parts, each with the laziest directive that still works, such as client:visible for anything below the fold.


FAQ: Astro Components vs Framework Components

No. Astro components render to HTML on the server and have no client-side runtime. The only JavaScript they send is from script tags you write yourself, which Astro bundles and deduplicates.

No. Without a client directive, a React, Vue or Svelte component is rendered to static HTML on the server and no JavaScript is sent. Only components with a client directive are hydrated.

No. Astro components can't be hydrated. The build logs a warning and renders the component as normal HTML. Use a script tag inside the component, or rewrite it as a framework component.

No. Framework component files can only import code for their own framework. You can pass an Astro component as a child or named slot to a framework component from inside an .astro file instead.

Only if the component isn't hydrated. Props of hydrated components are serialised into the HTML, and functions can't be serialised, so keep event handlers inside the framework component.

Yes. Install both integrations and import components from each into an .astro file. Each framework's runtime is only sent if at least one of its components is hydrated on that page.


Conclusion

Astro components and framework components look the same in a template, but they live in different places. Astro components exist only on the server: they can use any data, any secret and any JavaScript value, and they turn into HTML with no runtime. Framework components also render on the server, but with a client:* directive they continue running in the browser, bringing state, events and re-rendering at the cost of the framework's runtime and serialisable props.

The practical rule is to build pages from Astro components and add framework islands only where the interface genuinely needs client-side state. Pass data down as serialisable props, pass Astro-rendered markup in through slots, and remember that the nesting only goes one way: Astro files can contain framework components, never the reverse.

Tags :
Share :

Related Posts

What are Astro Actions and how do they work?

What are Astro Actions and how do they work?

Astro Actions are type-safe backend functions that you define once on the server and then call from the browser as if they were ordinary async fu

Dive Deeper
What are Astro adapters and which one should you choose?

What are Astro adapters and which one should you choose?

An Astro adapter is an integration that builds the server part of your project for a specific runtime, such as Node.js, Vercel Functions, Netlify

Dive Deeper
How to build API endpoints in Astro?

How to build API endpoints in Astro?

To build an API endpoint in Astro, create a .js or .ts file in src/pages/ and export a function named after an HTTP method, such as GET or

Dive Deeper