Astro directives are special attributes, written with a colon, that change how Astro renders an element or component. There are 16 in Astro 7: five client:* hydration directives, server:defer for server islands, set:html and set:text for raw content, class:list for classes, define:vars for passing values to CSS and scripts, three is:* directives that change processing, and three transition:* directives for view transitions. This page lists them all with the one-line rule and an example for each.
Key Takeaways
client:*directives only work on framework components (React, Vue, Svelte, Solid, Preact).server:deferonly works on Astro components and needs an adapter.set:htmlinserts unescaped HTML; never pass it untrusted input.is:inlineopts a script or style out of Astro’s processing entirely.
Hydration: client:*
| Directive | Hydrates | Use for |
|---|---|---|
client:load |
Immediately on page load | Above-the-fold controls |
client:idle |
When the browser is idle (requestIdleCallback) |
Important but not urgent widgets |
client:visible |
When the component enters the viewport | Anything below the fold |
client:media="(query)" |
When a media query matches | Mobile-only or desktop-only UI |
client:only="framework" |
In the browser only, no server render | Components that can’t run on the server |
<Counter client:load />
<SearchBox client:idle={{ timeout: 500 }} />
<Testimonials client:visible={{ rootMargin: '200px' }} />
<MobileNav client:media="(max-width: 768px)" />
<Map client:only="react" />
No directive means the component renders to HTML and ships no JavaScript. Choosing well is covered in lazy-loading islands.
Server islands: server:defer
<UserMenu server:defer>
<a slot="fallback" href="/login/">Sign in</a>
</UserMenu>
Renders the component on the server per request, after the page loads. The page itself can stay static and cached. Requires an adapter; without one you’ll see Cannot use server islands without an adapter.
Content: set:html and set:text
<article set:html={post.renderedHtml} />
<Fragment set:html={svgString} />
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
<p set:text={userInput} />
set:html injects a string without escaping. set:text sets escaped text content, like {value}. On <Fragment>, set:html renders with no wrapper element. Details in Fragment and set:html.
Classes: class:list
<a class:list={['btn', `btn-${variant}`, { active: isActive }, className]}>...</a>
Accepts strings, arrays and objects, flattens them, drops falsy values and removes duplicates. The cleanest way to combine a base class, variants and a caller’s class prop.
Values into CSS and scripts: define:vars
---
const { accent = '#2563eb' } = Astro.props;
---
<style define:vars={{ accent }}>
.badge { background: var(--accent); }
</style>
Each key becomes a CSS custom property scoped to the component instance. On a <script>, define:vars makes the script inline (unbundled). See define:vars for the trade-offs.
Processing control: is:*
| Directive | On | Effect |
|---|---|---|
is:global |
<style> |
Styles are not scoped to the component |
is:inline |
<script>, <style> |
Left exactly as written: no bundling, no deduplication, no TypeScript |
is:raw |
Any element | Children are treated as raw text, not Astro syntax |
<style is:global>
body { margin: 0; }
</style>
<script is:inline>
document.documentElement.dataset.theme = localStorage.getItem('theme') ?? 'light';
</script>
<code is:raw>{this is shown literally}</code>
is:inline is the right choice for the dark-mode script that must run before paint, and for third-party snippets. For everything else, processed scripts are smaller and deduplicated.
View transitions: transition:*
| Directive | Effect |
|---|---|
transition:name="hero-image" |
Pairs elements across pages so they animate into each other |
transition:animate="fade" |
Sets the animation: fade, slide, none, initial or a custom one |
transition:persist |
Keeps the element (and island state) across navigations |
<img src={cover} alt="" transition:name={`cover-${post.id}`} />
<main transition:animate="slide"><slot /></main>
<AudioPlayer client:load transition:persist />
These take effect with <ClientRouter /> from astro:transitions in the page head. transition:name also works with native cross-document view transitions in supporting browsers.
Which component types accept which directives
| Directive | Astro component | Framework component | HTML element |
|---|---|---|---|
client:* |
No | Yes | No |
server:defer |
Yes | No | No |
set:html, set:text |
No | No | Yes (and Fragment) |
class:list |
Passed as prop | Passed as prop | Yes |
define:vars |
No | No | <style>, <script> |
is:* |
No | No | Yes |
transition:* |
Yes | Yes | Yes |
Common mistakes
client:onlywithout a value fails: writeclient:only="react". See Missing hint on client:only.client:mediawithout a query fails. See Missing value for client:media.- A
client:*directive on an.astrocomponent does nothing useful; Astro components don’t hydrate. onclick={handler}on an HTML element in an Astro template doesn’t attach your frontmatter function; use a<script>.
For how these fit into components, read the Astro components guide. The formal definitions are in the directives reference.
FAQ
What is the default hydration directive in Astro?
There isn’t one. Without a client:* directive, a framework component renders to static HTML and ships no JavaScript.
Can I combine client:visible and transition:persist?
Yes. Directives from different groups can be combined on the same component.