Astro syntax is HTML plus JavaScript expressions in curly braces, with a TypeScript frontmatter block at the top fenced by ---. If you’ve written JSX it will feel familiar, with a few differences: attributes use HTML names (class, not className), components render once on the server, and a handful of directives control special behaviour. This cheatsheet covers every piece of .astro template syntax with a short example.
Key Takeaways
- Frontmatter (between
---) runs on the server; the template below it renders HTML.{expression}outputs escaped values;set:htmloutputs raw HTML.- Use
&&and ternaries for conditions,.map()for lists.- Write HTML attributes as HTML:
class,for,tabindex.
File structure
---
// Component script: runs at build time or on request
import Card from '../components/Card.astro';
interface Props { title: string }
const { title } = Astro.props;
const items = await fetchItems();
---
<!-- Component template -->
<h1>{title}</h1>
{items.map((item) => <Card {...item} />)}
<style>
h1 { font-size: 2rem; } /* scoped to this component */
</style>
<script>
console.log('runs in the browser, bundled once per page');
</script>
Expressions
<p>{user.name}</p>
<p>{price.toFixed(2)}</p>
<p>{new Date().getFullYear()}</p>
<p>{await getGreeting()}</p>
Top-level await works in frontmatter and in expressions. Values are HTML-escaped. null, undefined and false render nothing.
Attributes
<a href={url} target="_blank">Link</a>
<img src={src} alt={alt} width={800} />
<input disabled={isDisabled} /> <!-- false removes the attribute -->
<div {...attrs}></div> <!-- spread -->
<div class:list={['card', { active }]}></div>
Shorthand works when the variable name matches the attribute: <img {src} {alt} />.
Conditionals
{isLoggedIn && <a href="/account/">Account</a>}
{isLoggedIn ? <UserMenu /> : <a href="/login/">Sign in</a>}
{count > 0 && <span>{count} items</span>} <!-- compare numbers explicitly -->
More patterns in conditional rendering in Astro.
Loops
<ul>
{posts.map((post) => (
<li><a href={`/blog/${post.id}/`}>{post.data.title}</a></li>
))}
</ul>
Keys aren’t required; Astro renders once and never reconciles.
Fragments
<>
<dt>Term</dt>
<dd>Definition</dd>
</>
<Fragment set:html={htmlString} />
<Fragment slot="header">...</Fragment>
Slots
<!-- Card.astro -->
<div class="card">
<slot name="header" />
<slot /> <!-- default slot -->
<slot name="footer">Fallback</slot>
</div>
<!-- Using it -->
<Card>
<h3 slot="header">Title</h3>
<p>Body</p>
</Card>
Dynamic tags and components
---
const Tag = `h${level}`;
const Icon = icons[name];
---
<Tag>{text}</Tag>
<Icon />
Variables used as tags must start with a capital letter. Dynamic tags can’t take client:* directives.
Comments
<!-- HTML comment: sent to the browser -->
{/* JS comment: removed from the output */}
Raw HTML and text
<div set:html={trustedHtml} />
<p set:text={plainText} />
<code is:raw>{this stays literal}</code>
Differences from JSX
| JSX | Astro |
|---|---|
className |
class |
htmlFor |
for |
onClick={fn} |
Use a <script> or an island |
key needed in lists |
Not needed |
dangerouslySetInnerHTML |
set:html |
| Components re-render | Components render once |
| One root element | Any number of root elements |
style={{ color: 'red' }} |
style="color: red" or style={{ color: 'red' }} (both work) |
Whitespace
Since Astro 7, the default compressHTML: 'jsx' applies JSX whitespace rules: whitespace and line breaks around elements are removed. To keep a space between inline elements, write it explicitly:
<strong>Note:</strong>{' '}<span>details</span>
Things that only work in frontmatter
importstatementsexport const prerender,export async function getStaticPaths()return Astro.redirect(...)and other early returns
The Astro components guide explains props, slots and islands in depth, and the directives cheatsheet lists every name:value attribute. The Astro syntax reference is the formal spec.
FAQ
Is Astro syntax the same as JSX?
It’s close, but not the same. Astro uses HTML attribute names, renders once on the server, needs no keys, and allows multiple root elements.
Can I use TypeScript in the template?
Yes, in expressions. Type annotations belong in the frontmatter; the template uses the typed values.