
How to use conditional rendering in Astro templates?
To use conditional rendering in an Astro template, write a JavaScript expression in curly braces that evaluates to markup or to nothing. The two everyday forms are the logical AND, {isLoggedIn && <p>Welcome back</p>}, which renders the element only when the condition is truthy, and the ternary, {isLoggedIn ? <a href="/account">Account</a> : <a href="/login">Log in</a>}, which picks one of two branches. Anything more complex, such as a switch or several if statements, belongs in the component script between the --- fences, where you compute a value and then render it.
Astro has no v-if, {#if} or special template tags. Its templates use JSX-like expressions, so if you know how React handles conditions you already know most of this. There are a couple of differences, though, and a few falsy values that render in ways you might not expect. In this article you'll learn the && and ternary patterns, how to move logic into the frontmatter, how to render multiple elements conditionally, how to toggle attributes and classes, how to switch between tags and components, and which values to watch out for.
Logical AND: Render or Nothing
The && operator returns its right-hand side when the left is truthy, and its left-hand side otherwise. Astro renders the result:
---
// src/components/Welcome.astro
interface Props {
user?: { name: string };
}
const { user } = Astro.props;
---
{user && <p>Welcome back, {user.name}.</p>}
When user is undefined, the expression evaluates to undefined, which renders nothing. When it's an object, the paragraph is rendered. Because user.name is only evaluated inside the right-hand side, there's no risk of reading a property of undefined.
Wrap multi-line markup in parentheses so it's easy to read:
{user && (
<div class="profile">
<img src={user.avatar} alt="" width="40" height="40" />
<p>{user.name}</p>
</div>
)}
Watch Out for Zero
This is the trap everyone hits once. If the left-hand side is the number 0, && returns 0, and Astro renders it as text. I confirmed this in a build:
---
const count = 0;
const notifications: string[] = [];
---
{count && <p>You have {count} items</p>}
{notifications.length && <p>You have notifications</p>}
00
Both lines print a stray 0 on the page. The fix is to make the condition a real boolean:
{count > 0 && <p>You have {count} items</p>}
{notifications.length > 0 && <p>You have notifications</p>}
Boolean(count) or !!count also work, but a comparison states your intent more clearly.
Ternary: Either This or That
The conditional operator picks between two branches:
---
// src/components/AuthLink.astro
interface Props {
isLoggedIn: boolean;
}
const { isLoggedIn } = Astro.props;
---
{isLoggedIn ? <a href="/account">Account</a> : <a href="/login">Log in</a>}
It's the right tool for empty states, where you show a list or a message:
{results.length > 0 ? (
<ul>
{results.map((result) => <li>{result.title}</li>)}
</ul>
) : (
<p>No results found.</p>
)}
If you only need a fallback for a missing value, the nullish coalescing operator ?? is shorter. It renders the right-hand side only when the left is null or undefined:
<p>{post.subtitle ?? "Untitled"}</p>
Avoid nesting ternaries inside templates. Two levels are already hard to read; move that logic into the component script instead.
Moving Logic into the Frontmatter
The component script is ordinary TypeScript, so every control-flow statement is available there. Compute what you need, then keep the template simple:
---
// src/components/StatusBadge.astro
interface Props {
status: "draft" | "published" | "archived";
}
const { status } = Astro.props;
let label: string;
let tone: string;
switch (status) {
case "draft":
label = "Draft";
tone = "warning";
break;
case "archived":
label = "Archived";
tone = "muted";
break;
default:
label = "Live";
tone = "success";
}
---
<span class={`badge badge-${tone}`}>{label}</span>
For simple mappings, a lookup object is even tidier:
---
const labels = {
draft: "Draft",
published: "Live",
archived: "Archived",
} as const;
const { status } = Astro.props;
---
<span class="badge">{labels[status]}</span>
With a Props union type, TypeScript will warn you if you forget a key in the lookup, which a chain of if statements won't do. Typing props this way is covered in how to type component props with TypeScript.
When You Need if/else in the Template
If you really want if statements next to the markup, an immediately invoked function works:
{
(() => {
if (status === "draft") return <span>Draft</span>;
if (status === "archived") return <span>Archived</span>;
return <span>Live</span>;
})()
}
It's valid and occasionally useful, but most of the time a frontmatter variable or a small component is easier to read.
Returning Early from a Page
In a page that's rendered on demand, the frontmatter can return a Response to skip the template entirely, for example redirecting when a visitor isn't signed in:
---
// src/pages/dashboard.astro
export const prerender = false;
const user = Astro.locals.user;
if (!user) {
return Astro.redirect("/login");
}
---
<h1>Hello, {user.name}</h1>
This needs an adapter and on-demand rendering, and Astro.locals.user would be set by your own middleware. Protecting pages this way is covered in how to protect routes in Astro.
Rendering Several Elements Conditionally
The right-hand side of && or a ternary branch must be a single expression. To render several sibling elements without adding a wrapper, use a fragment:
{user.isAdmin && (
<>
<a href="/admin">Admin</a>
<a href="/admin/users">Users</a>
</>
)}
<>...</> and <Fragment>...</Fragment> render their children with no element of their own. This matters in places where a wrapper <div> would break the HTML, such as inside <ul>, <tr> or <head>. More on that in how to use the Fragment component.
Values That Render and Values That Don't
Since conditions often evaluate to non-element values, it's worth knowing exactly what Astro outputs for each. I rendered each value on its own in a template:
| Expression | Output |
|---|---|
{undefined} | Nothing |
{null} | Nothing |
{false} | Nothing |
{""} | Nothing |
{true} | The text true |
{0} | The text 0 |
{NaN} | The text NaN |
The surprising ones are at the bottom. true renders as text, unlike React, which renders nothing for true. So a pattern like {isReady || <Spinner />} would print true when isReady is true. Stick to && and ternaries with boolean conditions and you'll avoid all three.
Conditional Attributes
Attributes follow simple rules: false, null and undefined remove the attribute (with one exception, covered below), true adds it with no value, and anything else is converted to a string.
---
const isLoggedIn = true;
const isCurrent = false;
const isAdmin = false;
---
<button disabled={!isLoggedIn}>Go</button>
<input type="checkbox" checked={false} />
<a href="/blog" aria-current={isCurrent ? "page" : undefined}>Blog</a>
<div hidden={!isAdmin}>Admin tools</div>
<button>Go</button><input type="checkbox"><a href="/blog">Blog</a><div hidden>Admin tools</div>
Two things to remember:
- Boolean HTML attributes need real booleans:
disabled="false"is a string, and any value fordisableddisables the button. Usedisabled={false}. - ARIA attributes are the exception:
aria-expandedand friends expect the text"true"or"false", and Astro handles this for you. In a test build,aria-expanded={false}rendered asaria-expanded="false"rather than removing the attribute, so you can pass the boolean directly. Astro's types for these attributes accepttrue,false,"true"and"false", andastro checkactually rejects a generalstringsuch asString(isOpen). If you want an ARIA attribute gone, passundefined.
Conditional Classes with class:list
For classes, class:list is cleaner than string concatenation. It takes an array of strings, objects and nested arrays, adds object keys whose values are truthy, and skips false, null and undefined:
---
const user = { isAdmin: false };
const size = "lg";
---
<p class:list={["msg", { "msg-admin": user.isAdmin, "msg-user": !user.isAdmin }]}>
Hello
</p>
<button class:list={["btn", size === "lg" && "btn-lg"]}>Save</button>
The paragraph renders with class="msg msg-user". The second form, condition && "class", works because false entries are skipped.
Choosing Between Tags or Components
A condition can also decide which element or component to render. Assign it to a capitalised variable and use that as a tag:
---
// src/components/Title.astro
import Alert from "./Alert.astro";
import Notice from "./Notice.astro";
interface Props {
level?: 1 | 2;
urgent?: boolean;
}
const { level = 2, urgent = false } = Astro.props;
const Heading = level === 1 ? "h1" : "h2";
const Box = urgent ? Alert : Notice;
---
<Heading><slot /></Heading>
<Box message="Check your settings" />
The variable name must start with a capital letter, or Astro will treat it as a literal HTML tag called heading. Dynamic tags can't be used with client:* hydration directives, because Astro needs to know at build time which framework component to bundle. If you need that, use a ternary with the two components written out instead.
Conditional Slots and Wrappers
A related pattern is rendering a wrapper only when a slot has content. Astro.slots.has() returns true if the parent passed something for a slot:
{Astro.slots.has("footer") && (
<footer class="card-footer">
<slot name="footer" />
</footer>
)}
This avoids an empty <footer> with padding and borders on cards that don't need one. Slots, fallbacks and their edge cases are covered in what slots are and how to use named slots.
Conditions Run on the Server
Every condition in an Astro template is evaluated once, when the page is rendered: at build time for static pages, or per request for on-demand pages. The browser receives the HTML of the branch that was chosen and nothing else. That has two consequences:
- Server-side values are safe to use: you can branch on secrets, database results or cookies without exposing them, because only the resulting HTML is sent.
- Nothing changes after the page loads: if a condition depends on something in the browser, such as a click, the viewport or
localStorage, it has to be handled with a<script>or a framework component. A common approach is to render both states and toggle them with thehiddenattribute from a script.
For example, a dismissible banner can be rendered server-side and hidden from the client:
<div class="banner" id="promo">
<p>Free shipping this week.</p>
<button type="button" id="dismiss">Dismiss</button>
</div>
<script>
const banner = document.getElementById("promo");
document.getElementById("dismiss")?.addEventListener("click", () => {
banner?.setAttribute("hidden", "");
});
</script>
FAQ: Conditional Rendering in Astro
No. Astro templates use JavaScript expressions instead, mainly the logical AND operator and the ternary operator inside curly braces. For if, else or switch logic, compute a value in the component script and render that.
A condition like count && something returns 0 when count is zero, and Astro renders numbers as text. Use a real boolean such as count > 0 or array.length > 0 as the condition.
Set the attribute to an expression that evaluates to false, null or undefined when it should be absent. Those values remove the attribute, true adds it with no value, and other values are converted to strings. ARIA attributes are the exception: false is written out as the string "false", so use undefined to remove them.
Use the class:list directive with an object or a condition, passing an array that mixes plain class names with an object of conditional classes. Keys with truthy values are added and falsy entries are skipped.
Wrap them in a fragment, either the short form or the Fragment component. It groups the elements into a single expression without adding a wrapper element to the HTML.
No. Conditions run once on the server and only the chosen branch is sent as HTML. Use a script tag or a hydrated framework component for anything that should change after the page loads.
Conclusion
Conditional rendering in Astro is plain JavaScript inside curly braces. Use && when something should either render or not, a ternary when you need one of two branches, and ?? for simple fallbacks. When the logic grows, move it into the component script with switch, lookup objects or early returns, and keep the template focused on markup.
Most bugs come from values rather than syntax: 0, NaN and true all render as text, so make your conditions real booleans. Use false, null or undefined to remove attributes, class:list for classes, capitalised variables for dynamic tags, and remember that every condition is decided on the server. Anything that has to change in the browser needs a script or an island.


