
How to use the Fragment component in Astro?
The Fragment component in Astro groups several elements together without adding a wrapper element to the HTML. You write <Fragment>...</Fragment>, or the short form <>...</>, around the children, and only the children are rendered. Fragment is built in, so there's nothing to import. Use it when an expression must return a single value but you want to output several elements, when you want to send several elements to one named slot with <Fragment slot="name">, and when you want to inject an HTML string with set:html without an extra <div> around it.
A wrapper <div> is harmless in many places, but in others it produces invalid HTML or breaks a layout: inside <head>, <ul>, <dl>, <table> or a CSS grid where each child should be a grid item. Fragments solve all of those. In this article you'll learn where Fragments are needed and where they aren't, how the long and short forms differ, how to use them in conditions and loops, with named slots and with set:html and set:text, and the lowercase <fragment> trick for callback slots. Every output shown was produced by an Astro 7.4.2 build.
When You Don't Need a Fragment
Unlike React components, an Astro template can have several root elements:
---
// src/components/Intro.astro
---
<h1>Title</h1>
<p>Intro paragraph.</p>
That's valid and renders both elements. You don't need to wrap a component's whole template in a Fragment, even though many developers do so out of habit. Wrapping it is harmless; in my build, <Fragment><h1>Title</h1><p>Intro</p></Fragment> produced exactly <h1>Title</h1><p>Intro</p>.
Fragments become necessary in a handful of specific situations, covered below.
Fragment vs the Short Syntax
There are two ways to write a fragment:
<Fragment> | <> | |
|---|---|---|
| Renders a wrapper element | No | No |
| Needs an import | No, it's built in | No |
Accepts slot | Yes | No |
Accepts set:html and set:text | Yes | No |
| Typical use | Named slots, injecting HTML | Grouping in conditions and loops |
The short form can't take attributes at all, so whenever you need slot, set:html or set:text, use <Fragment>. Other attributes on a Fragment are simply dropped: in my build, <Fragment class="ignored"><b>frag</b></Fragment> rendered as <b>frag</b>.
Grouping Elements in Conditions
The right-hand side of && and each branch of a ternary must be a single expression. A fragment turns several elements into one:
---
// src/components/SeoTags.astro
interface Props {
title: string;
description: string;
image?: string;
}
const { title, description, image } = Astro.props;
---
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
{image && (
<>
<meta property="og:image" content={image} />
<meta name="twitter:card" content="summary_large_image" />
</>
)}
A wrapper element isn't an option here: a <div> inside <head> is invalid HTML, and since Astro 7's compiler no longer rearranges invalid markup, it would be passed through to the browser as written. With the fragment, the built <head> contains just the tags:
<head><title>Fragments</title><meta name="description" content="Group without wrappers."><meta property="og:title" content="Fragments"><meta property="og:image" content="/og.png"><meta name="twitter:card" content="summary_large_image"></head>
More patterns for conditions are in conditional rendering in Astro templates, and head tags for search engines are covered in SEO meta tags in Astro.
Returning Several Elements from a Loop
A description list needs <dt> and <dd> pairs as direct children of <dl>. Each loop iteration has to return both, without a wrapper:
---
const faq = [
{ q: "Is it fast?", a: "Yes." },
{ q: "Is it free?", a: "Also yes." },
];
---
<dl>
{faq.map((item) => (
<Fragment>
<dt>{item.q}</dt>
<dd>{item.a}</dd>
</Fragment>
))}
</dl>
<dl><dt>Is it fast?</dt><dd>Yes.</dd><dt>Is it free?</dt><dd>Also yes.</dd></dl>
The short <> form works just as well here. You don't need a key on the fragment as you would in React, because Astro renders the list once and never re-renders it. Loops are covered in depth in how to loop over arrays in Astro.
Other places this pattern helps:
- Table rows: returning several
<tr>elements per item, such as a row and a detail row. - Grid and flex layouts: where each element must be a direct child of the grid container.
- Inline separators: returning a comma and a link together, such as
<>{i > 0 && ", "}<a href={url}>{tag}</a></>.
Sending Several Elements to a Named Slot
To send content to a named slot, you add slot="name" to a child. If you want several elements in that slot, wrap them in a Fragment with the slot attribute rather than repeating it on each element or adding a wrapper:
---
// src/components/DataTable.astro
---
<table>
<thead><slot name="head" /></thead>
<tbody><slot /></tbody>
</table>
---
// src/pages/tools.astro
import DataTable from "../components/DataTable.astro";
const rows = [
{ name: "Astro", type: "Framework" },
{ name: "Vite", type: "Build tool" },
];
---
<DataTable>
<Fragment slot="head">
<tr><th>Name</th><th>Type</th></tr>
</Fragment>
{rows.map((row) => (
<tr><td>{row.name}</td><td>{row.type}</td></tr>
))}
</DataTable>
<table><thead><tr><th>Name</th><th>Type</th></tr></thead><tbody><tr><td>Astro</td><td>Framework</td></tr><tr><td>Vite</td><td>Build tool</td></tr></tbody></table>
A <div slot="head"> would have put a <div> inside <thead>, which is invalid. The fragment keeps the table structure correct. This is the main reason the long form exists, since <> can't carry a slot attribute. Slots are explained fully in what slots are and how to use named slots.
The same technique works for layouts. A page can add several tags to a layout's head slot in one go:
<BaseLayout title="About">
<Fragment slot="head">
<link rel="canonical" href="https://example.com/about/" />
<meta name="robots" content="noindex" />
</Fragment>
<h1>About</h1>
</BaseLayout>
Injecting HTML with set:html
set:html inserts an HTML string as markup instead of escaping it. On a normal element, the string becomes the element's content. On a Fragment, it's inserted with no element around it:
---
const snippet = "<p>Trusted <em>HTML</em> from the CMS</p>";
---
<Fragment set:html={snippet} />
<p>Trusted <em>HTML</em> from the CMS</p>
This is useful for HTML from a headless CMS, for pre-rendered Markdown, or for inline SVG markup where you don't want an extra <span> or <div>. It also works with the string returned by Astro.slots.render().
set:html doesn't escape anything, so it's only safe with content you trust or have sanitised. Never pass user input to it directly. Sanitising and the other safety rules are covered in how to render raw HTML safely with set:html.
set:text on a Fragment
set:text is the escaped counterpart. On a Fragment, it outputs text with no wrapper:
---
const plain = "Tom & Jerry <3";
---
<Fragment set:text={plain} />
<>{plain}</>
Tom & Jerry <3Tom & Jerry <3
Both lines produce the same escaped output, which is why set:text is rarely needed: a normal {expression} already escapes its value.
The Lowercase fragment Trick for Callback Slots
Astro supports passing a function as a slot's child, which a component can call with Astro.slots.render(name, [args]). When that function needs to go to a named slot, it has to be wrapped in an element that carries the slot attribute. Astro's documentation recommends a lowercase tag such as <fragment> for this, because a lowercase name isn't treated as a component:
<Shout message="slots!">
<fragment slot="message">
{(message: string) => <div>{message}</div>}
</fragment>
</Shout>
The wrapper is used only to deliver the callback to the message slot and isn't rendered. Note the difference: <Fragment> with a capital F is the component, <fragment> in lowercase is just a tag name used for transport here. This is an advanced pattern; for most components, plain slots are simpler.
Fragments in Markdown and MDX
In .md files, you can't use components at all, so Fragments don't apply. In .mdx files, the <> short syntax is part of the JSX that MDX supports, so you can use it to group elements inside expressions. For anything more involved, such as injecting a sanitised HTML string, it's usually cleaner to do the work in an Astro component and import that component into the MDX file. Writing posts in MDX is covered in how to write blog posts with MDX in Astro.
Common Mistakes
A few errors come up repeatedly when working with fragments:
- Putting
sloton the short syntax:<>can't take attributes, so there's no way to write a slot name on it. Switch to<Fragment slot="name">. - Expecting a Fragment to be styled: because it renders no element, a
class,idorstyleon a Fragment goes nowhere. If you need a styling hook, you need a real element. - Using a Fragment to fix invalid nesting: a Fragment removes a wrapper, but it doesn't make invalid markup valid. A
<div>inside a<p>is still invalid, whether or not a Fragment sits between them, and Astro 7 passes it through for the browser to handle. - Writing
<fragment>when you meant<Fragment>: the lowercase tag is only special as a transport for callback slots. Used anywhere else, it's treated as an unknown HTML tag rather than the built-in component. - Forgetting to close it: Astro 7's Rust compiler requires every non-void tag to be closed, including
</Fragment>and</>. Use the self-closing form<Fragment set:html={html} />when there are no children.
Quick Reference
- Grouping in a condition or loop:
<>...</>or<Fragment>...</Fragment>. - Several elements in a named slot:
<Fragment slot="name">...</Fragment>. - Unwrapped HTML string:
<Fragment set:html={html} />. - Unwrapped escaped text:
<Fragment set:text={text} />, or just{text}. - Callback for a named slot: lowercase
<fragment slot="name">. - Whole component template: no fragment needed; multiple root elements are allowed.
FAQ: The Astro Fragment Component
No. Fragment is built into Astro templates and can be used in any .astro file without an import. The short syntax also works without any setup.
Both render their children without a wrapper element. Only the Fragment component accepts attributes, so use it when you need slot, set:html or set:text.
No. Astro templates can have several root elements, so you don't need to wrap a component's template in a Fragment or a div.
Wrap them in a Fragment with the slot attribute, such as Fragment slot="footer". The elements go to that slot without an extra wrapper element.
No. A Fragment renders no element, so there's nothing to put a class on, and attributes other than slot and the set directives are dropped. Use a real element if you need styling.
No. Astro renders lists once to static HTML and never re-renders them, so fragments and elements in loops don't need a key attribute.
Conclusion
Fragment is a small, built-in tool for one job: grouping content without leaving a trace in the HTML. Reach for <> when a condition or loop needs to return several elements, and for <Fragment> when you need an attribute: slot to send several elements to a named slot, or set:html and set:text to inject content without a wrapper.
You don't need it around a component's whole template, because Astro allows multiple root elements, and you don't need keys on it in loops. Where it really matters is in places where a wrapper <div> would produce invalid HTML, such as <head>, lists and tables, which Astro 7's stricter compiler no longer rearranges for you.


