The Component Template
Everything below the closing --- fence of an .astro file is the component template — the markup that defines what the component renders. It looks like HTML, sprinkles in JSX-style expressions inside curly braces, and can embed other components. But it is not real JSX: Astro renders it once on the server to static HTML, so the rules are subtly different. Understanding those differences is the key to writing clean, fast Astro markup.
HTML first, expressions second
The template is HTML at heart. Plain tags, attributes, and text work exactly as you would expect in a .html file. What makes it dynamic is the ability to drop JavaScript expressions into curly braces {}, drawing on any variable, import, or value defined in the component script above.
---
const name = "Ada";
const year = "2026";
const tags = ["astro", "ssr", "islands"];
---
<article class="post">
<h1>Welcome, {name}</h1>
<p>Copyright {year}</p>
<ul>
{tags.map((tag) => <li>#{tag}</li>)}
</ul>
</article>
Anything inside {} is evaluated as a JavaScript expression at build (or request) time. The result is interpolated directly into the rendered HTML — no client-side runtime is involved.
Output:
<article class="post">
<h1>Welcome, Ada</h1>
<p>Copyright 2026</p>
<ul>
<li>#astro</li>
<li>#ssr</li>
<li>#islands</li>
</ul>
</article>
Dynamic attributes and conditional rendering
Expressions are not limited to text content. You can compute attribute values, build class lists, and conditionally render fragments of markup using ordinary JavaScript operators.
---
const href = "/blog";
const isActive = true;
const user = { name: "Grace", avatar: "/grace.png" };
---
<a href={href} class:list={["nav-link", { active: isActive }]}>
Blog
</a>
{user ? <img src={user.avatar} alt={user.name} /> : <span>Guest</span>}
{isActive && <span class="badge">You are here</span>}
Astro provides the class:list directive as a first-class way to assemble conditional class names from arrays and objects. For conditionals, the JavaScript ternary (? :) and logical-and (&&) patterns work just as they do in JSX.
Tip: A
false,null, orundefinedexpression renders nothing, whiletruealso renders nothing — so{isActive && <Badge />}is safe and idiomatic for “show only when truthy.”
How the template differs from real JSX
The familiarity is intentional, but Astro templates are HTML-flavored, not React-flavored. The differences below trip up developers arriving from React, so it is worth committing them to memory.
| Feature | Astro template | React JSX |
|---|---|---|
| Attribute casing | HTML names: class, for, tabindex | DOM names: className, htmlFor, tabIndex |
| Comments | <!-- HTML comment --> | {/* JS comment */} |
| Multiple root nodes | Allowed without a wrapper | Needs a fragment or single root |
| Inline event handlers | Not hydrated (onClick is just an attribute) | Real client-side listeners |
| Whitespace | Preserved as authored | Collapsed by the compiler |
| Re-rendering | Renders once on the server | Re-renders on state change |
Because Astro uses standard HTML attribute names, you write class (not className) and for (not htmlFor). You can also return multiple top-level elements from a template without wrapping them, since the output is plain HTML.
Fragments and the Fragment component
When you need to group elements without adding a wrapper element — for example inside a .map() that should emit siblings — use Astro’s built-in <Fragment> or the shorthand <>.
---
const rows = [
{ label: "Speed", value: "Fast" },
{ label: "JS shipped", value: "Zero by default" },
];
---
<dl>
{rows.map((row) => (
<Fragment>
<dt>{row.label}</dt>
<dd>{row.value}</dd>
</Fragment>
))}
</dl>
<Fragment> is also the canonical way to inject a raw HTML string via the set:html directive without an extra DOM node.
Escaping, raw HTML, and slots
By default Astro escapes interpolated values, protecting you from accidental HTML injection. When you intentionally need to render trusted markup — say, HTML produced by a Markdown renderer — opt in explicitly with set:html.
---
const richText = "<strong>Bold</strong> and <em>italic</em>";
---
<!-- Escaped: shows the literal tags as text -->
<p>{richText}</p>
<!-- Rendered as real HTML -->
<p set:html={richText} />
Templates can also expose insertion points for child content using the <slot /> element, which is how parent components pass markup into a component’s layout. That composition mechanism is covered in the nested components page.
Best practices
- Treat the template as HTML: use
class,for, and lowercase attribute names rather than their React equivalents. - Keep expressions in
{}small and declarative — compute complex values in the script fence and reference the result in the template. - Prefer
class:listover manual string concatenation for conditional classes. - Use
<Fragment>(or<>) to emit sibling elements inside loops without polluting the DOM with wrappers. - Reserve
set:htmlfor trusted, sanitized content; rely on Astro’s default escaping everywhere else. - Remember the template renders once on the server — inline
onclickattributes are static text, so reach for a framework island when you need real interactivity.