Components

Astro components (.astro files) are the building blocks of every Astro site. They render to static HTML with no client-side runtime.

Anatomy of an .astro file

Every .astro file has two parts separated by a code fence (---):

---
// Component Script (runs on the server, never in the browser)
import Layout from '../layouts/Layout.astro';
const title = "Hello";
const items = await fetch('https://api.example.com/items').then(r => r.json());
---

<!-- Component Template (HTML with expressions) -->
<Layout title={title}>
  <h1>{title}</h1>
  <ul>
    {items.map(item => <li>{item.name}</li>)}
  </ul>
</Layout>

<style>
  /* Scoped CSS - only applies to this component */
  h1 { color: navy; }
</style>

The frontmatter (between ---) is server-side JavaScript/TypeScript. It runs at build time (static) or request time (on-demand). It never reaches the browser.

The template below is HTML with JSX-like expressions. It is not JSX - it is closer to HTML with interpolation.

Props

Components receive props via Astro.props:

---
interface Props {
  title: string;
  count?: number;
}
const { title, count = 0 } = Astro.props;
---
<h2>{title} ({count})</h2>

Usage:

<Card title="News" count={5} />

Slots

Slots let components accept child content, like React’s children:

---
// Layout.astro
---
<html>
  <body>
    <header>Site Header</header>
    <main>
      <slot />  <!-- default slot -->
    </main>
    <aside>
      <slot name="sidebar" />  <!-- named slot -->
    </aside>
  </body>
</html>

Usage:

<Layout>
  <p>This goes in the default slot</p>
  <nav slot="sidebar">Sidebar content</nav>
</Layout>

Key difference from React: Astro uses <slot /> (web component standard), not {children}.

Scoped styles

<style> tags in .astro files are scoped by default. Astro adds unique class names to keep styles isolated:

<style>
  h1 { color: red; }  /* Only affects h1 in THIS component */
</style>

For global styles, use is:global:

<style is:global>
  body { margin: 0; }
</style>

Or import a CSS file:

---
import '../styles/global.css';
---

Expressions and conditionals

The template supports JavaScript expressions inside {}:

---
const show = true;
const items = ['a', 'b', 'c'];
---

{show && <p>Visible</p>}
{show ? <p>Yes</p> : <p>No</p>}

<ul>
  {items.map(item => <li>{item}</li>)}
</ul>

Key differences from JSX

JSX (React)Astro
classNameclass
htmlForfor
{children}<slot />
style={{ color: 'red' }}style="color: red;"
onChangeUse <script> or client island
Runs in browserRuns on server only

Client-side scripts

For interactivity without a framework, use <script> tags:

<button id="btn">Click me</button>

<script>
  document.getElementById('btn').addEventListener('click', () => {
    alert('Clicked!');
  });
</script>

Scripts are bundled and deduplicated by Vite. They run in the browser.

Dynamic HTML with set:html

To inject raw HTML (careful with XSS):

---
const rawHtml = '<em>formatted</em> content';
---
<div set:html={rawHtml} />

Async data fetching

The frontmatter supports top-level await:

---
const response = await fetch('https://api.example.com/data');
const data = await response.json();
---
<p>{data.title}</p>

No useEffect, no useState, no hydration. The fetch runs on the server, and the result renders to static HTML.