client:only
The client:only directive tells Astro to skip server rendering entirely for a UI framework component and render it exclusively in the browser. Unlike the other client:* directives, which send pre-rendered HTML and then hydrate it, client:only sends no markup for the component at all — the framework boots up on the client and produces the DOM from scratch. This is the escape hatch for components that simply cannot run on the server.
When you need client:only
Some components reference browser-only APIs at render time — window, document, localStorage, the Canvas or WebGL context, or libraries that touch the DOM during their initial render (charting, mapping, rich-text editors). Server-rendering those components throws because those globals don’t exist in Node. client:only sidesteps the problem by never rendering on the server.
---
// src/pages/dashboard.astro
import LiveChart from "../components/LiveChart.jsx";
---
<h1>Analytics</h1>
<LiveChart client:only="react" />
Because nothing is rendered on the server, you must explicitly tell Astro which framework runtime to load — Astro cannot infer it from server output the way it does for client:load or client:visible.
Warning:
client:onlyproduces a content layout shift and a flash of empty space until the JS loads and runs. Prefer a hydrating directive (client:load,client:visible) whenever the component can render on the server, and reserveclient:onlyfor components that genuinely cannot.
Specifying the framework
The value passed to client:only is the name of the renderer integration, not the file extension. Use the matching string for whichever framework the component is written in:
| Framework | Directive value |
|---|---|
| React | client:only="react" |
| Preact | client:only="preact" |
| Vue | client:only="vue" |
| Svelte | client:only="svelte" |
| SolidJS | client:only="solid-js" |
---
import MapWidget from "../components/MapWidget.vue";
import Editor from "../components/Editor.svelte";
---
<MapWidget client:only="vue" />
<Editor client:only="svelte" />
If you omit the value, Astro cannot determine the runtime to ship and will fail the build. The corresponding integration must also be registered in astro.config.mjs:
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import vue from "@astrojs/vue";
export default defineConfig({
integrations: [react(), vue()],
});
Handling the loading gap
Since the component renders nothing on the server, you typically want a placeholder so the layout doesn’t jump. Astro provides a slot fallback pattern using the component’s own framework, but the simplest reliable approach is to reserve space and let the component manage its own loading state once it mounts.
---
import LiveChart from "../components/LiveChart.jsx";
---
<div class="chart-slot" style="min-height: 320px;">
<LiveChart client:only="react" />
</div>
Inside the component, render a skeleton until the browser-only dependency is ready:
// src/components/LiveChart.jsx
import { useEffect, useState } from "react";
export default function LiveChart() {
const [data, setData] = useState(null);
useEffect(() => {
// window/localStorage are safe here — this only runs in the browser
const cached = window.localStorage.getItem("metrics");
if (cached) setData(JSON.parse(cached));
else fetch("/api/metrics").then((r) => r.json()).then(setData);
}, []);
if (!data) return <p>Loading chart…</p>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
Output:
Loading chart… ← shown until JS loads and effect runs
{ ← replaced once data resolves
"visits": 1280,
"signups": 42
}
How it differs from hydrating directives
The key distinction is what the server sends. With a hydrating directive, the server emits real HTML and the client “takes over” that markup. With client:only, the server emits a placeholder marker only.
| Behavior | client:only | client:load |
|---|---|---|
| Server-rendered HTML | None | Yes |
| Framework value required | Yes | No |
| Works without browser globals at render | N/A (never runs on server) | Must be SSR-safe |
| First paint shows content | No (empty until JS) | Yes (immediately) |
| SEO crawlable content | No | Yes |
Tip: Because
client:onlycontent is not in the server HTML, search engines and no-JS users see nothing. Never put SEO-critical or above-the-fold core content behindclient:only.
Best Practices
- Use
client:onlyonly for components that break during SSR; everything else should hydrate so users get instant HTML. - Always pass the explicit framework value (
"react","vue","svelte", etc.) — omitting it breaks the build. - Register the matching framework integration in
astro.config.mjsbefore using its directive value. - Reserve layout space with a
min-heightwrapper or in-component skeleton to avoid content layout shift. - Keep
client:onlyislands small and isolated so the JS-only region stays narrow and the rest of the page remains zero-JS. - Move data fetching and
window/documentaccess into effects or lifecycle hooks so the logic only runs client-side, where it’s safe.