To render a string of HTML in Astro, use the set:html directive: <div set:html={htmlString} />. Astro escapes normal expressions like {htmlString}, so tags show up as text; set:html inserts the string as raw HTML instead. When you don’t want a wrapper element, put the directive on <Fragment>: <Fragment set:html={htmlString} />.
Key Takeaways
{value}is always escaped.set:html={value}is not.<Fragment>renders its children with no wrapper element. Use it for raw HTML, grouping, and filling slots.- Never pass user-supplied HTML to
set:htmlwithout sanitising it.set:textis the escaped counterpart, handy on elements that would otherwise take children.
Why {html} shows tags as text
---
const html = '<strong>Bold</strong> text';
---
<p>{html}</p>
<!-- Output: <p><strong>Bold</strong> text</p> -->
Escaping by default protects you from cross-site scripting. When you have trusted HTML, such as CMS rich text or Markdown you’ve rendered yourself, you need to opt in.
set:html
---
const html = '<strong>Bold</strong> text';
---
<p set:html={html} />
<!-- Output: <p><strong>Bold</strong> text</p> -->
It also accepts a promise, so you can fetch and render in one line:
<article set:html={fetch('https://cms.example.com/page/about.html').then((r) => r.text())} />
Fragment: no wrapper
<Fragment set:html={html} />
<!-- Output: <strong>Bold</strong> text -->
<Fragment> is built in, so there’s nothing to import. The short syntax <>...</> also works for grouping, but directives need the full <Fragment> tag.
JSON-LD: the most common use
Structured data needs unescaped JSON inside a script tag:
---
const schema = { '@context': 'https://schema.org', '@type': 'Organization', name: 'Example Studio' };
---
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
Writing {JSON.stringify(schema)} as a child would escape the quotes and break the JSON. The Astro SEO checklist has a full article schema component.
Fragment for slots
Named slots target one element. To send several elements to one slot without adding a div, wrap them in a Fragment:
<Card>
<Fragment slot="header">
<h3>Pro plan</h3>
<p class="price">$29</p>
</Fragment>
Everything in Starter, plus priority builds.
</Card>
Fragment for grouping in expressions
When a .map() or condition needs to return several siblings:
<dl>
{terms.map((t) => (
<Fragment>
<dt>{t.term}</dt>
<dd>{t.definition}</dd>
</Fragment>
))}
</dl>
A div here would make the <dl> invalid HTML.
set:text
set:text sets text content, escaped, like {value}. It’s mainly useful for elements where you’d rather keep the value in an attribute position:
<style set:text={cssString} />
Safety
set:html does no sanitising. If any part of the string comes from users (comments, form fields, query parameters), sanitise it first with a library such as DOMPurify (with a server DOM like jsdom) or sanitize-html, and allow only the tags you need. HTML from your own build pipeline or a CMS your team controls is generally fine.
For where these fit among props, slots and layouts, see the Astro components guide, and for showing elements only under some conditions, conditional rendering in Astro. The directives reference lists every template directive.
FAQ
How do I render raw HTML in Astro?
Use set:html on an element or on <Fragment>: <Fragment set:html={html} />.
Do I need to import Fragment in Astro?
No. <Fragment> is available in every .astro file without an import.