
What do client:load, client:idle, and client:visible mean in Astro?
In Astro, client:load, client:idle and client:visible are client directives that decide when a UI framework component's JavaScript is downloaded and the component is hydrated. client:load hydrates immediately on page load, client:idle waits until the browser is idle after the initial load, and client:visible waits until the component scrolls into the viewport. All three render the component to HTML on the server first, so visitors see the content straight away; the directive only controls when it becomes interactive. Without any client:* directive, a React, Vue or Svelte component is rendered to static HTML and ships no JavaScript at all.
These directives are the main lever you have over how much JavaScript an Astro page runs and when it runs it. Choosing well keeps the main thread free for the things that matter first. In this article you'll learn what each directive does under the hood, the options client:idle and client:visible accept, the related client:media and client:only directives, how to choose between them, and the rules and mistakes that trip people up.
Why Astro Needs Client Directives
Astro renders every component to HTML on the server by default, including components written for React, Vue, Svelte, Preact or Solid. That's what gives Astro pages their small JavaScript payloads, as covered in the post on Astro's island architecture.
The catch is that a server-rendered React button can't respond to clicks. To make it interactive, the browser needs the component's code and the framework runtime, and it needs to run them against the existing HTML. That process is called hydration. A client directive opts a single component into hydration and tells Astro how urgently to do it.
---
// src/pages/index.astro
import Counter from '../components/Counter';
---
<!-- Static HTML only, no JavaScript sent -->
<Counter label="Static" />
<!-- Hydrated as soon as the page loads -->
<Counter label="Load" client:load />
<!-- Hydrated when the browser is idle -->
<Counter label="Idle" client:idle />
<!-- Hydrated when it scrolls into view -->
<Counter label="Visible" client:visible />
The component used throughout this article is a plain React counter:
// src/components/Counter.tsx
import { useState } from 'react';
interface Props {
label: string;
start?: number;
}
export default function Counter({ label, start = 0 }: Props) {
const [count, setCount] = useState(start);
return (
<button type="button" onClick={() => setCount(count + 1)}>
{label}: {count}
</button>
);
}
All four counters look identical when the page first appears. Only the three with a directive will ever respond to a click.
The Three Main Directives Compared
| Directive | When JavaScript loads | Priority | Good for |
|---|---|---|---|
client:load | Immediately on page load | High | Visible UI that must work right away |
client:idle | After the initial load, when the browser is idle | Medium | Useful but non-urgent UI |
client:visible | When the component enters the viewport | Low | Content below the fold, heavy widgets |
client:load
client:load loads and hydrates the component as soon as the page loads. Use it for elements that people are likely to interact with immediately and that would feel broken if they didn't respond: a "Buy now" button at the top of a product page, a search box in the header, or a navigation menu toggle on mobile.
<BuyButton client:load />
The trade-off is that its JavaScript competes with everything else the page is doing during start-up. If several large components all use client:load, they can delay each other and the rest of the page's work.
client:idle
client:idle waits until the page has finished its initial load and the browser reports it has spare time through requestIdleCallback(). The component is still server-rendered and visible, it just becomes interactive a moment later.
<ShowHideButton client:idle />
This suits components that are on screen but not the first thing someone will touch: a "copy code" button, a newsletter form in the sidebar, or a theme switcher.
client:idle accepts a timeout option in milliseconds. It's passed to requestIdleCallback(), so the component hydrates within that time even if the browser never becomes idle:
<ShowHideButton client:idle={{ timeout: 500 }} />
Use the timeout when a component must be usable within a predictable window on busy pages. The Astro docs say browsers without requestIdleCallback() fall back to the document load event; the runtime in Astro 7.4.2 that I inspected uses a short setTimeout (the timeout value, or 200ms if none is set) in that case. Either way, the component still hydrates.
client:visible
client:visible doesn't load anything until the component enters the viewport, which it detects with an IntersectionObserver. If the visitor never scrolls that far, the JavaScript is never downloaded.
<HeavyImageCarousel client:visible />
This is the right default for anything below the fold: comment sections, image carousels, interactive charts, embedded maps, or a "related posts" widget at the bottom of an article.
The downside is a short delay between the component appearing and it becoming interactive, because the download only starts when it becomes visible. On slow connections a fast scroller might click before hydration finishes. The rootMargin option fixes this by starting the work early:
<HeavyImageCarousel client:visible={{ rootMargin: '200px' }} />
With rootMargin: '200px', hydration starts when the component is within 200 pixels of the viewport. I tested this on a page with a 3,000px spacer and two counters below it, one with rootMargin: '400px' and one without. Before scrolling, neither had hydrated and the browser hadn't requested any component JavaScript. Scrolling to 300px short of the counters hydrated only the one with the margin. Scrolling the rest of the way hydrated the other.
Two More Directives Worth Knowing
The three directives in the title cover most cases, but Astro has two others.
client:media
client:media hydrates the component once a CSS media query matches:
<SidebarToggle client:media="(max-width: 50em)" />
It's built for components that only matter at certain screen sizes, such as a mobile menu button. On a wide desktop window the JavaScript never loads. If you resize the window so the query starts matching, the component hydrates at that point.
If the element is already hidden with CSS at other sizes, client:visible is often simpler, because a hidden element is never "visible" either.
client:only
client:only skips server rendering entirely and renders the component in the browser on page load. You must tell Astro which framework the component uses, because it never runs the component on the server to find out:
<MapWidget client:only="react" />
It's for components that can't run on the server, usually because they touch window, localStorage or other browser APIs during rendering. It has trade-offs of its own, covered in detail in how to use client:only.
What the Directive Produces in the HTML
It helps to see what Astro actually outputs. A hydrated component is wrapped in an <astro-island> custom element that records the component's URL, its serialised props and the directive. This is trimmed output from a test page with one counter per variant (... marks attributes I removed for brevity):
<button type="button">No directive<!-- -->: <!-- -->0</button>
<astro-island ... component-url="/_astro/Counter.BpKsUxkk.js" ... props="{"label":[0,"Load"]}" ssr client="load" opts="{"name":"Counter","value":true}" await-children><button type="button">Load<!-- -->: <!-- -->0</button><!--astro:end--></astro-island>
<astro-island ... client="idle" opts="{"name":"Counter","value":{"timeout":500}}" ...>
<astro-island ... client="visible" opts="{"name":"Counter","value":{"rootMargin":"200px"}}" ...>
<astro-island ... client="media" opts="{"name":"Counter","value":"(max-width: 50em)"}" ...>
A few things stand out:
- The static component is just HTML: no wrapper, no script, no props.
- Directive options are stored in
opts: thetimeout,rootMarginand media query values end up there. - Small inline loaders: Astro adds a tiny script for each directive type used on the page, which decides when to call the island's hydrate function. A page that only uses
client:visibledoesn't get theclient:idleloader. - The framework runtime is shared: if five React islands are on a page, React is downloaded once.
How to Choose the Right Directive
A practical way to decide is to ask three questions, in order:
- Does this component need JavaScript at all? If it only displays data, leave the directive off. A static React component costs nothing in the browser. For simple toggles, a
<script>tag is often enough, as shown in adding interactivity without a framework. - Will someone interact with it within the first second or two? If yes, and it's above the fold, use
client:load. - Is it on screen at load? If yes but not urgent, use
client:idle. If it's further down the page, useclient:visible, adding arootMarginwhen the component is heavy or the first interaction matters.
Some typical choices:
| Component | Directive |
|---|---|
| Header search with autocomplete | client:load |
| Mobile navigation toggle | client:media="(max-width: 48em)" or client:load |
| Theme switcher in the header | client:idle |
| Copy-to-clipboard buttons in an article | client:idle or client:visible |
| Comments section at the end of a post | client:visible={{ rootMargin: '300px' }} |
Chart that uses window during render | client:only="react" |
| Pricing table that never changes | No directive |
When in doubt, start lazy and move up. It's easy to change client:visible to client:idle if testing shows people reach the component before it's ready, and much harder to notice that client:load everywhere is slowing your page.
Rules and Common Mistakes
Directives Only Work on Framework Components
Astro components (.astro files) render to HTML and have no client runtime, so they can't be hydrated. I tried <LikeCounter client:load /> on an Astro component in Astro 7.4.2; the build completed but printed this, and nothing was hydrated:
You are attempting to render <LikeCounter client:load />, but LikeCounter is an Astro component. Astro components do not render in the client and should not have a hydration directive. Please use a framework component for client rendering.
To make an Astro component interactive, add a <script> tag inside it instead.
The Directive Must Be Visible to the Compiler
Directives are handled at compile time. They must be written directly on a component that's imported into the .astro file. Two patterns don't work:
- Spreading attributes:
<Counter {...attrs} />won't hydrate even ifattrscontainsclient:load. - Dynamic tags: hydration directives aren't supported on dynamic tags like
<Component client:load />whereComponentis a variable, or on components passed to MDX through thecomponentsprop.
Props Must Be Serialisable
Props passed to a hydrated component are serialised into the HTML. Plain objects, strings, numbers, arrays, Map, Set, Date, RegExp, BigInt, URL, typed arrays and Infinity are supported. Functions are not, so you can't pass an onClick handler from an Astro page into a hydrated React component. Define the handler inside the component instead.
Every Directive Ships the Framework
The first island using a framework pulls in that framework's runtime. In my test build, the React renderer chunk alone was about 66 KB gzipped. A single client:idle on one small widget still costs that download, so it's worth asking whether a framework is the right tool for very small interactions.
FAQ: Astro Client Directives
Astro renders it to static HTML on the server and sends no JavaScript for it. It will look correct but won't respond to clicks, state changes or effects.
No. client:load renders the component on the server first and then hydrates it immediately. client:only skips server rendering, so the component only appears after its JavaScript has loaded and run in the browser.
No. The component is still rendered to HTML on the server, so search engines see its content. Only the JavaScript that makes it interactive is delayed.
No. Use one client directive per component instance. You can use different directives on different instances of the same component on one page.
Yes. Integrations can register custom client directives with the addClientDirective option in the astro:config:setup hook, for example to hydrate a component on hover or on a custom event.
Yes, for framework components imported directly into the MDX file. They don't work on components passed in through the components prop when rendering MDX content.
Conclusion
Client directives are how Astro turns static framework components into interactive islands, one component at a time. client:load hydrates immediately, client:idle waits for a quiet moment after the page loads, and client:visible waits until the component is on screen. client:media and client:only cover the special cases of screen-size-specific and browser-only components.
Treat the directive as a performance decision rather than a formality. Leave it off when a component doesn't need to be interactive, use client:load sparingly for what must work instantly, and let client:idle and client:visible handle the rest. Add timeout and rootMargin when you need tighter control, and check the built HTML for <astro-island> elements whenever you want to confirm exactly what's being hydrated.


