Skip to content
Astro as components 4 min read

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, or undefined expression renders nothing, while true also 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.

FeatureAstro templateReact JSX
Attribute casingHTML names: class, for, tabindexDOM names: className, htmlFor, tabIndex
Comments<!-- HTML comment -->{/* JS comment */}
Multiple root nodesAllowed without a wrapperNeeds a fragment or single root
Inline event handlersNot hydrated (onClick is just an attribute)Real client-side listeners
WhitespacePreserved as authoredCollapsed by the compiler
Re-renderingRenders once on the serverRe-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:list over manual string concatenation for conditional classes.
  • Use <Fragment> (or <>) to emit sibling elements inside loops without polluting the DOM with wrappers.
  • Reserve set:html for trusted, sanitized content; rely on Astro’s default escaping everywhere else.
  • Remember the template renders once on the server — inline onclick attributes are static text, so reach for a framework island when you need real interactivity.
Last updated June 14, 2026
Was this helpful?