Skip to content
Astro as components 3 min read

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.

ConceptAstro componentReact/Vue component
Runs whereServer (build/SSR)Client (hydrated)
Ships JS by defaultNoYes
Re-renders on state changeNoYes
File extension.astro.jsx / .vue
InteractivityOpt-in via client:*Built-in

Tip: Reach for an .astro component 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 .astro components 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 prefer client:visible or client:idle over client:load.
  • Type your props with a Props interface 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.
Last updated June 14, 2026
Was this helpful?