
What are .astro files and how do they work?
.astro files are Astro's own component format. Each one has two parts: a component script between --- fences at the top, where you write JavaScript or TypeScript to import components, read props and fetch data, and a component template below it, which is HTML extended with JSX-like {expressions}, other components, <slot /> elements and special directives. Astro compiles the file into a function that runs on the server, at build time or on request, and outputs plain HTML. The component itself has no client-side runtime, so none of its code is sent to the browser unless you add a <script> tag or an interactive island.
Because the template is a superset of HTML, any valid HTML is also a valid Astro component, which makes the format easy to learn. In this article you'll learn the anatomy of an .astro file, the template syntax and how it differs from JSX, how styles and scripts inside a component are processed, how slots and props connect components, what changed with Astro 7's Rust compiler, and what the compiled output looks like. Everything was checked against Astro 7.4.2.
The Anatomy of an .astro File
Here's a small but typical component:
---
// src/components/Card.astro
interface Props {
title: string;
href: string;
}
const { title, href } = Astro.props;
---
<article class="card">
<h2><a href={href}>{title}</a></h2>
<slot />
</article>
<style>
.card {
padding: 1rem;
border: 1px solid #ddd;
border-radius: 0.5rem;
}
h2 {
margin: 0 0 0.5rem;
}
</style>
It has three areas:
- Component script (between the
---fences): TypeScript that runs on the server. Here it declares the props and reads them fromAstro.props. - Template (the markup): what the component renders.
{title}andhref={href}insert values from the script, and<slot />marks where child content goes. - Style (optional): CSS scoped to this component.
You can also add a <script> tag for browser-side behaviour, which is covered below.
The Component Script
The fence was inspired by Markdown frontmatter, and its contents are "fenced in": the code runs on the server and never reaches the browser. That makes it safe for secrets, database queries and heavy computation. You can use imports, top-level await, TypeScript types and any npm package that runs in Node.js. The script runs once each time the component renders. The details have their own article: what the component script is in an Astro component.
The Template
The template determines the HTML output. If you write only HTML, you get exactly that HTML (minus insignificant whitespace). Variables and expressions from the script go in curly braces.
Template Syntax
Expressions and Attributes
---
const name = 'Astro';
const url = 'https://astro.build';
---
<h1>Hello {name}!</h1>
<a href={url} class="external">Visit {name}</a>
<p title={`About ${name}`}>Template literals work in attributes too.</p>
Values are escaped automatically, so a string containing <strong> is rendered as text, not markup. To insert trusted HTML on purpose, use the set:html directive, explained in how to use set:html in Astro.
Conditionals and Loops
Astro uses the same patterns as JSX: logical &&, ternaries and .map():
---
const isActive = true;
const items = ['Dog', 'Cat'];
---
<a class:list={['nav-link', { active: isActive }]} href="/">Home</a>
{items.length > 0 ? <ul>{items.map((i) => <li>{i}</li>)}</ul> : <p>None</p>}
The compiled output is:
<a class="nav-link active" href="/">Home</a><ul><li>Dog</li><li>Cat</li></ul>
class:list is a directive that builds a class string from strings, objects and arrays, skipping falsy values. Loops and conditionals each have a dedicated article: conditional rendering in Astro and looping over arrays in Astro.
Fragments, Dynamic Tags and Comments
- Fragments:
<Fragment>or<>...</>group elements without a wrapper, which is useful withset:htmlor named slots. - Dynamic tags: assign a tag name or component to a capitalised variable, such as
const Heading = 'h2', and render<Heading>. Client directives don't work on dynamic tags. - Comments: HTML comments (
<!-- -->) are included in the output; JavaScript comments ({/* */}) are removed. Use the latter for notes you don't want visitors to see.
Differences From JSX
The syntax looks like JSX, but it isn't React:
.astro template | JSX (React) | |
|---|---|---|
| Class attribute | class | className |
| Other attributes | Standard HTML, kebab-case (data-value) | camelCase props (dataValue) |
| Root elements | Any number | One root, or a fragment |
| Event handlers | Not supported in templates | onClick={handler} |
| Re-rendering | Never: renders once on the server | Re-renders on state change |
| Children | <slot /> | props.children |
The event handler point catches everyone once. Attributes on HTML elements are converted to strings, so <button onClick={handleClick}> won't work. Use a <script> tag with addEventListener instead.
How Styles Work
A <style> tag in an .astro file is scoped by default. Astro adds a data-astro-cid-* attribute to the component's elements and rewrites the selectors to match. Here's the Card component's output:
<style>.card[data-astro-cid-yk4hkwyg]{border:1px solid #ddd;border-radius:.5rem;padding:1rem}h2[data-astro-cid-yk4hkwyg]{margin:0 0 .5rem}
</style>
...
<article class="card" data-astro-cid-yk4hkwyg><h2 data-astro-cid-yk4hkwyg><a href="/blog/astro-island-architecture" data-astro-cid-yk4hkwyg>Islands</a></h2><p>How partial hydration works.</p></article>
Two things stand out. The plain h2 selector only matches headings inside this component, so it can't leak into the rest of the site. And the <p> passed in through the slot has no scope attribute, because it belongs to the parent component, not to Card.
To write global CSS from a component, use <style is:global> or wrap individual selectors in :global(). Pass server values into CSS custom properties with define:vars. The styling options are covered in scoped CSS in Astro components.
How Scripts Work
A <script> tag with no attributes (other than src) is processed:
- TypeScript is supported and compiled away.
- Imports of local files and npm packages are bundled.
- The result is a module, with
type="module". - Deduplication: if the component appears ten times on a page, the script is included once.
- Inlining: small scripts are inlined into the HTML instead of becoming a separate request.
This component:
---
// src/components/ThemeToggle.astro
---
<button type="button" data-theme-toggle>Toggle theme</button>
<script>
const buttons = document.querySelectorAll<HTMLButtonElement>('[data-theme-toggle]');
buttons.forEach((button) => {
button.addEventListener('click', () => {
document.documentElement.classList.toggle('dark');
});
});
</script>
used twice on a page produced one minified, type-stripped script:
<button type="button" data-theme-toggle>Toggle theme</button><script type="module">document.querySelectorAll(`[data-theme-toggle]`).forEach(e=>{e.addEventListener(`click`,()=>{document.documentElement.classList.toggle(`dark`)})});</script><button type="button" data-theme-toggle>Toggle theme</button>
Because the script runs only once, it uses querySelectorAll to wire up every instance. For per-instance behaviour, define a custom element and put the logic in connectedCallback().
Adding any other attribute, or is:inline, opts out of processing. The script is then output exactly as written, wherever you wrote it, and repeated for each instance. That's the right choice for third-party snippets or scripts in public/.
Props and Slots
Components receive data through props, which are attributes on the component tag, and content through slots:
---
// src/pages/scripts.astro
import Base from '../layouts/Base.astro';
import ThemeToggle from '../components/ThemeToggle.astro';
import Card from '../components/Card.astro';
---
<Base title="Scripts">
<ThemeToggle />
<Card title="Islands" href="/blog/astro-island-architecture">
<p>How partial hydration works.</p>
</Card>
</Base>
The interface Props in Card isn't just documentation. Running npx astro check (after installing @astrojs/check and typescript) catches a missing prop:
src/pages/bad.astro:4:2 - error ts(2322): Type '{ title: string; }' is not assignable to type 'IntrinsicAttributes & Props'.
Property 'href' is missing in type '{ title: string; }' but required in type 'Props'.
4 <Card title="Missing href" />
Named slots (<slot name="footer" /> filled by slot="footer") and fallback content are covered in Astro slots explained.
Pages, Layouts and Components Are All .astro Files
There's no separate format for pages. An .astro file in src/pages/ is a page and gets a URL; the same file anywhere else is a component you import. Layouts are simply components that render a full <html> document with a <slot /> for the page.
A page can also do things a component can't, because it controls the response: set headers, set the status code or return a redirect when rendered on demand. Components can read the request but can't change response headers, because by then Astro has already started streaming the HTML.
Astro 7: The Rust Compiler
Astro 7 replaced the Go-based compiler with a Rust-based one. It's faster, and it's stricter in two ways.
Unclosed Tags Are Errors
Every non-void element must be closed. This file:
<div>
<p>Hello world
</div>
fails the build:
[CompilerError] Expected corresponding JSX closing tag for 'p'.
Void elements like <br>, <img>, <input> and <hr> still don't need closing tags.
Invalid Nesting Is No Longer Fixed for You
The old compiler silently restructured invalid HTML, such as a <div> inside a <p>. The Rust compiler passes your markup through as written and leaves the browser to handle it, which may change how such a page renders. If your output looks different after upgrading, check for invalid nesting.
Whitespace Follows JSX Rules
Astro 7 also changed the default compressHTML setting to 'jsx', which removes whitespace between elements the way React does. This template:
<p>
<span>hello</span>
<em>world</em>
</p>
<p><span>hello</span>{" "}<em>world</em></p>
renders as:
<p><span>hello</span><em>world</em></p><p><span>hello</span> <em>world</em></p>
The first paragraph lost its space. Add {" "} where inline elements need a space between them, or set compressHTML: true in your config to get the previous HTML-aware behaviour back.
Editor Support
The official Astro VS Code extension, and the Astro language server used by other editors, give you syntax highlighting, autocompletion, type checking of props and expressions, and formatting. Add npx astro check to your CI to catch type errors in .astro files before they're deployed. The default build also checks types when your tsconfig.json uses the strict or strictest preset.
FAQ: .astro Files
No. The template syntax borrows JSX expressions, but .astro files use standard HTML attributes such as class, allow multiple root elements, render once on the server and don't support inline event handlers.
Code in the component script never does. It runs on the server during the build or on request. Only script tags in the template, and framework components with a client directive, send JavaScript to the browser.
Yes. The component script is TypeScript by default, and processed script tags support TypeScript too. Declare a Props interface to type your props, and run astro check to find type errors.
The Rust compiler in Astro 7 requires every non-void element to have a matching closing tag. Add the missing closing tag shown in the error location; void elements such as img and br are fine without one.
No, they're scoped to the component by default using data-astro-cid attributes. Use style is:global, or the :global() selector, when you need rules that apply across the site.
No. Astro components have no client runtime, so client directives don't work on them. Add a script tag inside the component, or use a framework component as an island.
Conclusion
An .astro file is a server-rendered component made of a fenced script and an HTML-like template, with optional scoped styles and processed scripts. The script prepares data, the template turns it into HTML using expressions, components and slots, and Astro compiles the whole thing to a function whose output is plain markup. None of the component's own code reaches the browser.
Astro 7's Rust compiler makes the format a little stricter, with closed tags, honest nesting and JSX-style whitespace, but the model hasn't changed. If you can write HTML, you can write .astro files; add props for data, slots for content, a <style> for presentation and a <script> only when the page genuinely needs to do something in the browser.


