client:visible
Astro ships zero JavaScript by default, and client:visible is the directive that pushes that philosophy the furthest. Instead of hydrating an interactive island as soon as the page loads, client:visible waits until the component actually scrolls into the user’s viewport. This is ideal for content that lives below the fold, where shipping and executing JavaScript up front would be pure waste. The result is a faster initial load and a smaller hydration budget on the critical rendering path.
How it works
When you add client:visible to a framework component, Astro renders its static HTML on the server as usual, but defers loading the component’s JavaScript. On the client, Astro registers an IntersectionObserver that watches the island’s root element. The moment any part of the element crosses into the viewport, the observer fires, Astro fetches the component’s JS, and hydration runs — attaching event listeners and making the island interactive.
Because the markup is already present from server rendering, users see and can read the content immediately. Only the interactivity is deferred, so there is no layout shift or flash of missing content.
---
// src/pages/index.astro
import Header from "../components/Header.jsx";
import CommentForm from "../components/CommentForm.jsx";
---
<Header client:load />
<article>
<!-- long-form content the user reads first -->
</article>
<!-- Hydrates only once it scrolls into view -->
<CommentForm client:visible />
In this example, the header hydrates eagerly with client:load because it is above the fold and needs to be interactive right away, while the comment form near the bottom of a long article stays dormant until the reader actually reaches it.
Tuning with rootMargin
By default the observer triggers exactly when the element edge enters the viewport. You can hydrate slightly before the element is visible by passing a rootMargin value, which expands the observer’s bounding box. This is useful to start hydration just ahead of the scroll so the island is ready by the time the user sees it.
---
import Carousel from "../components/Carousel.svelte";
---
<!-- Start hydrating 200px before the element scrolls into view -->
<Carousel client:visible={{ rootMargin: "200px" }} />
The value uses the same syntax as the CSS margin shorthand and maps directly to the IntersectionObserver rootMargin option.
Tip: A small positive
rootMargin(100–300px) is often the sweet spot. It hides the hydration latency behind the scroll motion without eagerly loading components the user may never reach.
Comparing client directives
client:visible is one of several hydration strategies. Choosing the right one comes down to when interactivity is needed.
| Directive | When it hydrates | Best for |
|---|---|---|
client:load | Immediately on page load | Above-the-fold, critical UI |
client:idle | When the main thread is idle | Low-priority but visible widgets |
client:visible | When the element enters the viewport | Below-the-fold content |
client:media | When a CSS media query matches | Responsive, device-specific UI |
client:only | On the client only, no SSR | Browser-only components |
For a page full of independent below-the-fold widgets — image galleries, embedded maps, social share buttons — client:visible keeps the initial JavaScript payload close to zero and amortizes hydration across the scroll.
A complete example
Here is a Vue island that fetches data only after it becomes interactive, paired with client:visible so the network request never fires for users who don’t scroll down.
---
// src/pages/dashboard.astro
import LiveStats from "../components/LiveStats.vue";
---
<main>
<h1>Dashboard</h1>
<section class="hero"><!-- above the fold --></section>
<LiveStats client:visible={{ rootMargin: "150px" }} />
</main>
// src/components/LiveStats.vue (script setup)
import { ref, onMounted } from "vue";
const stats = ref<{ users: number } | null>(null);
onMounted(async () => {
const res = await fetch("/api/stats");
stats.value = await res.json();
console.log("LiveStats hydrated and data loaded");
});
Output:
LiveStats hydrated and data loaded
The onMounted hook — and therefore the fetch — only runs after the island hydrates, which only happens when it scrolls into view.
Best practices
- Reserve
client:loadfor genuinely above-the-fold interactivity; default toclient:visiblefor everything below the fold. - Add a modest
rootMargin(around 100–300px) so islands finish hydrating just before the user sees them. - Avoid
client:visibleon elements that are visible on initial load — there is no deferral benefit and you add observer overhead; useclient:loadorclient:idleinstead. - Keep server-rendered HTML meaningful on its own so the pre-hydration state is fully readable and accessible.
- Measure the impact with Lighthouse or a Total Blocking Time audit;
client:visibleshould reduce JavaScript executed on first load. - Combine many small
client:visibleislands rather than one giant hydrated tree to spread hydration cost across the scroll.