What you'll learn
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.
