Photo by Eric Er on Unsplash

On this page

This chapter follows Creating an Astro project.

Astro components are the building block of pages, layouts, and UI. They run on the server, at build time for static routes or per request for on-demand routes, and produce HTML. Nothing in the component script reaches the browser unless you put it in a <script> tag.

The component script and the template

A component has two parts separated by a code fence. Above the fence is the component script, written in TypeScript or JavaScript. Below it is the template, which is HTML with expressions.

src/components/Greeting.astro
---
const name = 'Astro';
const items = ['Pages', 'Layouts', 'Components'];
const response = await fetch('https://example.com/api/status');
const status = await response.json();
---
<h2>Hello, {name}</h2>
<p>Status: {status.ok ? 'up' : 'down'}</p>
<ul>
{items.map((item) => <li>{item}</li>)}
</ul>

The script runs once per render, and top-level await is allowed. A fetch here happens on the server, so API keys used in the script never reach the browser. For a static route the request happens during the build, and the result is baked into the HTML.

Template expressions

Curly braces evaluate JavaScript inside the template. Conditionals and lists use ordinary expressions rather than template-specific syntax:

---
const user = { name: 'Ada', admin: true };
const tags = ['astro', 'islands'];
const html = '<strong>trusted markup</strong>';
---
{user.admin && <span class="badge">Admin</span>}
{tags.length > 0 ? <p>{tags.join(', ')}</p> : <p>No tags</p>}
<p class:list={['card', { 'card--admin': user.admin }]}>Profile</p>
<div set:html={html} />

Two directives are worth knowing early. class:list builds a class string from strings, arrays, and objects. set:html injects a raw HTML string without escaping; use it only with content you trust, since everything else in braces is escaped by default.

Props

Components receive attributes as Astro.props. Declare an interface named Props and Astro type-checks every place the component is used:

src/components/Card.astro
---
interface Props {
title: string;
href: string;
featured?: boolean;
}
const { title, href, featured = false } = Astro.props;
---
<a class:list={['card', { featured }]} href={href}>
<h3>{title}</h3>
</a>
src/pages/index.astro
---
import Card from '../components/Card.astro';
---
<Card title="Routing" href="/routing/" featured />

Props are plain values. Functions can be passed to other Astro components, but there are no event handlers in the React sense, because the component never runs in the browser.

Slots

A <slot /> marks where child content goes. Named slots let a layout accept several regions, and content inside the slot tag is the fallback when nothing is passed:

src/layouts/BaseLayout.astro
---
interface Props {
title: string;
}
const { title } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{title}</title>
</head>
<body>
<header><slot name="header">Default header</slot></header>
<main><slot /></main>
</body>
</html>
src/pages/about.astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="About">
<nav slot="header">Custom navigation</nav>
<p>This paragraph fills the default slot.</p>
</BaseLayout>

A layout is nothing more than a component with a default slot that wraps a page. This site’s article pages use the same pattern, with a named hero slot for the page header.

Scoped styles

A <style> block in a component is scoped automatically. Astro adds a unique attribute to the component’s elements and rewrites selectors to match, so h3 in Card.astro cannot affect headings elsewhere.

<h3>Scoped heading</h3>
<style>
h3 {
color: rebeccapurple;
}
</style>

Use :global(...) for selectors that must reach outside, such as Markdown content rendered into a slot, and <style is:global> for a whole global block. Pass server values into CSS with define:vars:

---
const accent = '#0b7285';
---
<style define:vars={{ accent }}>
a {
color: var(--accent);
}
</style>

Client-side scripts

A <script> tag in a component is the way to run code in the browser without a UI framework. Astro bundles it, supports TypeScript and imports, and includes it once per page even if the component appears many times.

src/components/CopyButton.astro
<button data-copy="npm create astro@latest">Copy</button>
<script>
document.querySelectorAll<HTMLButtonElement>('[data-copy]').forEach((button) => {
button.addEventListener('click', () => {
navigator.clipboard.writeText(button.dataset.copy ?? '');
});
});
</script>

Because the script runs once for the page, select elements by attribute rather than assuming one instance. Server values reach the script through data-* attributes, as above. <script is:inline> opts out of processing and emits the tag exactly as written.

Images

astro:assets optimizes local images at build time. It infers width and height to prevent layout shift, converts to modern formats, and lazy-loads by default:

---
import { Image } from 'astro:assets';
import hero from '../assets/hero.png';
---
<Image src={hero} alt="Diagram of an Astro page with islands" />

Keep images you want optimized in src/, not public/; files in public/ are served as-is. Remote images work too, but need explicit width and height and an allowed domain in the config. <Picture /> produces several formats in a <picture> element.

Next: Routing in Astro.

Check your understanding

Question 1 of 7

0 of 7 checked

  1. 1. Where does a fetch in the component script run?

Search articles

Type a word to search published articles.

Open the search page