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 |
|---|---|
className | class |
htmlFor | for |
{children} | <slot /> |
style={{ color: 'red' }} | style="color: red;" |
onChange | Use <script> or client island |
| Runs in browser | Runs 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.