Astro Components
Astro components are the fundamental building blocks of any Astro project. A component lives in a .astro file and combines a component script fence (JavaScript or TypeScript that runs on the server at build time) with an HTML-like template below it. Crucially, Astro components render to plain HTML and ship zero JavaScript to the browser by default — making them ideal for content-heavy, fast-loading sites where interactivity is the exception, not the rule.
The two-part anatomy
Every .astro component has at most two parts: an optional component script enclosed in a code fence (---), and a template. The script runs entirely on the server; nothing inside it is sent to the client. The template is everything below the closing fence and is what actually becomes HTML.
---
// Component Script (runs on the server, never shipped to the browser)
const greeting = "Hello";
const items = ["Astro", "is", "fast"];
---
<!-- Component Template (rendered to HTML) -->
<section class="card">
<h1>{greeting}, world!</h1>
<ul>
{items.map((item) => <li>{item}</li>)}
</ul>
</section>
<style>
.card { padding: 1rem; border-radius: 8px; }
</style>
The fence is JSX-like in the template (expressions in {}, .map() for lists) but is not React — there is no virtual DOM and no re-rendering. The output is static markup computed once on the server.
Why the component model matters
Astro’s design follows a philosophy of server-first rendering with islands of interactivity. Because components produce HTML rather than client-side JavaScript, pages are smaller and render faster. When you genuinely need interactivity, you opt in explicitly using UI framework components and client:* directives — so the cost of JavaScript is paid only where it delivers value.
| Concept | Astro component | React/Vue component |
|---|---|---|
| Runs where | Server (build/SSR) | Client (hydrated) |
| Ships JS by default | No | Yes |
| Re-renders on state change | No | Yes |
| File extension | .astro | .jsx / .vue |
| Interactivity | Opt-in via client:* | Built-in |
Tip: Reach for an
.astrocomponent first. Only introduce a framework component (React, Svelte, Vue) when a piece of UI truly needs client-side state or event handling.
Composing and reusing components
Components import other components in the script fence and use them like custom HTML tags in the template. This composition is how you build entire pages from small, reusable pieces.
---
import Card from "../components/Card.astro";
import Footer from "../components/Footer.astro";
const posts = [
{ title: "Islands Architecture", slug: "islands" },
{ title: "Content Collections", slug: "collections" },
];
---
<main>
{posts.map((post) => (
<Card title={post.title} href={`/blog/${post.slug}`} />
))}
</main>
<Footer />
Components accept props via Astro.props, and can render arbitrary child markup through the <slot /> element — the two primary mechanisms for making components configurable and composable.
Adding interactivity with islands
When you need client-side behavior, render a UI framework component and hydrate it with a client:* directive. Everything else on the page stays static HTML.
---
import Counter from "../components/Counter.jsx";
import StaticHero from "../components/StaticHero.astro";
---
<StaticHero />
<!-- Only this component hydrates in the browser -->
<Counter client:visible />
client:visible defers hydration until the component scrolls into view; client:load hydrates immediately; client:idle waits for the main thread to be idle. Each directive is a deliberate, scoped opt-in to JavaScript.
Fetching data at the top level
Because the script fence runs on the server, you can await directly in it — including fetching from APIs or querying content collections — without exposing keys or adding client-side requests.
---
const response = await fetch("https://api.example.com/stats");
const stats = await response.json();
---
<p>We have {stats.userCount} happy developers.</p>
Output:
<p>We have 12480 happy developers.</p>
The HTML is fully rendered before it reaches the browser — no loading spinners, no client-side fetch.
Best practices
- Default to
.astrocomponents and let pages stay zero-JS unless interactivity is genuinely required. - Keep the script fence focused on data and imports; push presentation into the template.
- Reach for framework components plus a
client:*directive only for true islands of interactivity, and preferclient:visibleorclient:idleoverclient:load. - Type your props with a
Propsinterface so consumers get editor autocomplete and build-time safety. - Scope styles inside the component with
<style>blocks — Astro scopes them automatically to avoid leaks. - Compose pages from small, single-purpose components and use
<slot />for flexible content injection.