Quick Answer

Astro is a web framework that renders components to HTML at build time and ships zero JavaScript by default. Interactive widgets are opt-in "islands": you mark a React, Vue, Svelte, or Solid component with a client:* directive and only that component's JavaScript is sent to the browser. It suits content-heavy sites like blogs, docs, and marketing pages.

The .astro component

An .astro file has two parts: a frontmatter script between --- fences that runs at build time on the server, and a template below it that looks like HTML with JSX-style expressions.

---
const build = new Date().toISOString();
const posts = ["astro-basics", "islands", "ssr"];
---
<h1>Blog</h1>
<p>Built at {build}</p>
<ul>
  {posts.map((slug) => <li>{slug}</li>)}
</ul>

The frontmatter can import components, read files, query a database, or call an API - it is ordinary JavaScript that executes once during the build. The output is plain HTML: no client-side runtime, no hydration, no framework code in the page. A file at src/pages/blog.astro becomes /blog, and src/pages/blog/[slug].astro handles dynamic routes. This is the default, and it is the whole point - a page of .astro components ships an HTML document and nothing else.

Islands: interactivity on demand

When a page needs interactivity, you use a framework component and tell Astro to hydrate it. That hydrated component is an "island" - a small piece of client-side JavaScript in an otherwise static page.

---
import Like from "../components/Like.jsx";   // a React component
---
<Like start={5} />                 <!-- rendered to HTML, no JS shipped -->
<Like start={10} client:load />    <!-- hydrates as soon as the page loads -->
<Like start={20} client:visible /> <!-- hydrates when scrolled into view -->

Without a directive, the React component is server-rendered to HTML and its JavaScript is never sent - the button appears but does nothing. Add client:load and Astro wraps it in an <astro-island> element and loads that component plus the React runtime. Each island is independent; the rest of the page stays static.

The directives control when hydration happens: client:load immediately, client:idle when the browser is free, client:visible on scroll, and client:only="react" to skip server rendering entirely and render only in the browser.

Use React, Vue, and Svelte together

Astro is not a UI framework of its own for interactivity - it renders other frameworks. Add an integration and you can use components from several at once:

npx astro add react
npx astro add svelte

This installs the renderer and updates astro.config.mjs. Now one page can import a React data grid, a Svelte chart, and Vue form controls, each as its own island. They do not share state directly - each is isolated - but they can communicate through browser events, a shared store such as nanostores, or URL state.

Why would you mix them? Usually not on purpose. It happens when you want to reuse an existing component or a third-party widget built for a specific framework without porting it. The cost is that each framework you actually hydrate adds its runtime to the pages that use it, so keep the number of frameworks you ship small even though you technically can load several.

The directive that ships nothing

Client directives only do something on framework components. Put one on an .astro component and it is silently ignored - that component was always going to be server-only HTML.

---
import Card from "../components/Card.astro";
---
<Card title="Broken" client:load />   <!-- does nothing -->

The build prints a warning rather than failing:

You are attempting to render <Card client:load />, but Card 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.

A related trap: props passed to an island must be JSON-serializable. Numbers, strings, plain objects, and arrays are fine; functions and class instances are not, so you cannot pass an event-handler callback down into an island the way you would between two React components. And a plain <script> tag in an .astro file does not see frontmatter variables - that scope only exists at build time. Use <script define:vars={{ myVar }}> to pass a value into client-side script.

What actually ships to the browser

The islands model is worth it because you can see the difference in the built output. Take a page with three copies of the same React component - one plain, two with client:* directives - and look inside dist/.

The plain <Like start={5} /> compiles to exactly this, with no wrapper and no script:

<button>Likes: 5</button>

The client:load copy compiles to an <astro-island> element that references two files - the component bundle and the framework runtime - which the browser then downloads. The client:only copy renders an empty island; its button only appears after the browser executes it.

So the JavaScript cost of a page is the sum of its islands and their frameworks, not the whole page. A blog post with one comment widget ships the bytes for that widget. The article text, navigation, and footer cost nothing at runtime because they were never anything but HTML. That is the mechanism behind Astro's performance numbers.

When Astro is the wrong choice

Astro is built for content: sites that are mostly text and images with islands of interactivity. Blogs, documentation, marketing sites, portfolios, and news sites are the sweet spot, and Astro can pull that content from Markdown files, a CMS, or an API at build time.

It is a poor fit for an application that is interactive edge to edge - a dashboard, an email client, a design tool, a live collaborative editor. If almost every element on screen has state and updates constantly, the island model works against you: you end up with islands everywhere, coordinating through workarounds, reinventing what a single-page-app framework gives you directly. For those, reach for Next.js, Remix, SvelteKit, or plain React.

Astro does support server-side rendering and API endpoints for the dynamic parts of a content site - forms, search, personalization - so "static by default" does not mean "static only". But the framework's shape rewards content-first sites, and fighting that shape is usually a sign you picked the wrong tool.

Frequently Asked Questions

Does Astro replace React? No. Astro is a page framework; React is one of the UI libraries it can render inside islands. You still write React components for interactive parts - Astro just controls how and when they load, and renders everything else as plain HTML.
Why does my React component render but the buttons do nothing? You imported it without a client directive, so Astro server-rendered it to static HTML and shipped no JavaScript for it. Add client:load, client:idle, or client:visible to hydrate it in the browser.
Can Astro build dynamic, server-rendered pages? Yes. With an SSR adapter, Astro can render pages per request and expose API endpoints. The default is static output, but you can switch the output mode or opt individual routes into server rendering.
Can I use Tailwind and TypeScript with Astro? Yes to both. 'npx astro add tailwind' wires up Tailwind, and TypeScript works in .astro frontmatter and in framework components with no extra setup. Astro also type-checks with 'astro check'.
Is a plain script tag in an .astro file processed by Astro? Yes. By default Astro bundles and optimizes it like any module and it runs in the browser. But it cannot see frontmatter variables; use define:vars to pass values in, or fetch data from the script itself.