
How to use client:only for browser-only components in Astro?
To use client:only in Astro, add it to a framework component and pass the name of the component's framework as its value, for example <Chart client:only="react" />. Astro then skips server rendering for that component completely: nothing is rendered at build time or on the server, and the component is rendered from scratch in the browser as soon as the page loads. That makes it the right tool for components that can't run outside a browser, such as ones that read window, localStorage or navigator while rendering, or that wrap a library which touches the DOM on import. Add a child with slot="fallback" to show placeholder content until the component appears.
Every other client directive renders the component to HTML first and hydrates it later. client:only is the exception, and that has real costs as well as benefits. In this article you'll learn the exact syntax for each framework, how fallback content works, what the built HTML looks like, the errors you'll see when you get it wrong, the trade-offs compared with other directives, and the alternatives that often work better.
Why Some Components Need client:only
Astro renders framework components on the server so visitors get HTML immediately. During that render there's no browser: no window, no document, no localStorage. Code that uses those while rendering fails.
Here's a React component that shows the visitor's local time and counts their visits in localStorage:
// src/components/LocalTime.tsx
import { useEffect, useState } from 'react';
export default function LocalTime() {
const [now, setNow] = useState(() => new Date());
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const visits = Number(localStorage.getItem('visits') ?? '0') + 1;
useEffect(() => {
localStorage.setItem('visits', String(visits));
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id);
}, []);
return (
<p>
{now.toLocaleTimeString()} in {zone}. Visit number {visits}.
</p>
);
}
With client:load, Astro tries to render it during the build. In my test with Astro 7.4.2 on Node.js 26, the build reported:
[ERROR] [build] Caught error rendering /broken: TypeError: Cannot read properties of undefined (reading 'getItem')
Even where rendering doesn't crash, the server-rendered time and time zone would be the server's, not the visitor's. This component only makes sense in a browser.
The client:only Syntax
Pass the framework name as a string:
---
// src/pages/index.astro
import LocalTime from '../components/LocalTime';
---
<LocalTime client:only="react" />
The value is required because Astro normally discovers which renderer a component needs by rendering it on the server. With client:only it never does that, so you have to say. The accepted values for the official integrations are:
| Framework | Directive |
|---|---|
| React | client:only="react" |
| Preact | client:only="preact" |
| Svelte | client:only="svelte" |
| Vue | client:only="vue" |
| Solid | client:only="solid-js" |
If you leave the value out, the build fails with a clear message. This is what I saw for <LocalTime client:only />:
[NoClientOnlyHint] Unable to render `LocalTime`. When using the `client:only` hydration strategy, Astro needs a hint to use the correct renderer.
Hint:
Did you mean to pass `client:only="react|preact|solid-js|vue (jsx)"`? See https://docs.astro.build/en/reference/directives-reference/#clientonly for more information on `client:only`.
Passing the wrong framework is worse, because it isn't caught at build time. When I used client:only="vue" on the React component, the build succeeded and the output pointed the island at Vue's renderer. The component would fail when the browser tried to render a React component with Vue. Double-check the value whenever you copy a line from another component.
Showing Fallback Content
A client:only component has no server HTML, so by default its space on the page is empty until JavaScript runs. Add any element with slot="fallback" to show something in the meantime:
---
// src/pages/index.astro
import LocalTime from '../components/LocalTime';
---
<LocalTime client:only="react">
<p slot="fallback">Loading your local time...</p>
</LocalTime>
The fallback can be plain text, a skeleton, a spinner or another Astro component. Here's the built HTML for the example above, with the long attribute values shortened:
<astro-island ... component-url="/_astro/LocalTime.B82Fx-Qt.js" ... props="{}" ssr client="only" opts="{"name":"LocalTime","value":"react"}" await-children><p>Loading your local time...</p><template data-astro-template="fallback"><p>Loading your local time...</p></template><!--astro:end--></astro-island>
The fallback is the only content inside the island. The component's own markup is absent, because it was never rendered. When I loaded the page in Chrome, the fallback was replaced within moments by:
11:03:22 AM in Asia/Dhaka. Visit number 1.
Give the fallback roughly the same size as the final component. Otherwise the content below will jump when the component renders, which counts against your Cumulative Layout Shift score.
How client:only Compares with Other Directives
client:load | client:only | |
|---|---|---|
| Server renders HTML | Yes | No |
| Visible before JavaScript runs | Yes | Only the fallback |
| Content seen by search engines and no-JS visitors | Yes | Only the fallback |
| Can use browser APIs while rendering | No | Yes |
| When it runs in the browser | Immediately on page load | Immediately on page load |
| Framework must be specified | No | Yes |
The loading behaviour of client:only is the same as client:load: the component's JavaScript is fetched as soon as the page loads. There's no client:only equivalent of client:visible, so a browser-only component below the fold still loads straight away. The full set of directives is covered in what client:load, client:idle and client:visible mean.
The Trade-Offs
client:only solves the server rendering problem by giving up the main benefits of server rendering:
- Slower first content: the visitor sees nothing (or only the fallback) until the framework and component download, parse and render.
- No HTML for search engines: crawlers that don't run JavaScript only see the fallback. Don't use
client:onlyfor content you want indexed. - No content without JavaScript: if a script fails to load, the fallback is all anyone sees.
- Layout shift: unless the fallback reserves the right amount of space.
Because of this, treat client:only as a targeted fix for components that truly can't render on the server, not as a way to silence any server rendering error.
Good Uses for client:only
- Components that read browser storage during render: like the visit counter above, or UI driven by persisted state where server HTML would mismatch the stored value. That mismatch problem is shown in sharing state between islands.
- Libraries that touch the DOM on import: many map, chart, rich text editor and canvas libraries access
windowas soon as they're imported, which breaks server rendering. - Values that differ per browser: the visitor's time zone, locale, screen size or device capabilities.
- Purely client-side apps embedded in a page: an admin tool or calculator that has no meaningful server-rendered state.
Alternatives to Try First
Often you can keep server rendering and still use browser APIs, as long as you don't use them during render.
Move Browser Code into an Effect
Effects and mount hooks only run in the browser, after hydration. In React:
// src/components/VisitCount.tsx
import { useEffect, useState } from 'react';
export default function VisitCount() {
const [visits, setVisits] = useState<number | null>(null);
useEffect(() => {
const next = Number(localStorage.getItem('visits') ?? '0') + 1;
localStorage.setItem('visits', String(next));
setVisits(next);
}, []);
return <p>{visits === null ? 'Counting visits...' : `Visit number ${visits}.`}</p>;
}
This renders "Counting visits..." on the server and updates once hydrated with any directive, including client:visible. The equivalents are onMounted() in Vue and $effect() or onMount() in Svelte.
Import Browser-Only Libraries Lazily
If a library breaks on import, load it inside the effect with a dynamic import():
useEffect(() => {
let cancelled = false;
import('some-browser-only-library').then((lib) => {
if (!cancelled) lib.init(containerRef.current);
});
return () => {
cancelled = true;
};
}, []);
The component still renders a container on the server, and the library only loads in the browser. Replace some-browser-only-library and init() with your library's actual API.
Skip the Framework
If all you need is a bit of browser-only behaviour, a <script> in an Astro component runs only in the browser by design and needs no framework at all.
Use a Server Island for Per-User Data
If the reason for client:only is that the content is personalised, rather than browser-specific, a server island may be a better fit. It renders on the server per request, with access to cookies and your database, and doesn't ship a framework.
Using client:only with Other Frameworks
The pattern is the same for every framework. A Svelte component that reads the viewport size:
<!-- src/components/ViewportSize.svelte -->
<script lang="ts">
let width = $state(window.innerWidth);
</script>
<svelte:window onresize={() => (width = window.innerWidth)} />
<p>Your viewport is {width}px wide.</p>
<ViewportSize client:only="svelte">
<p slot="fallback">Measuring your screen...</p>
</ViewportSize>
For a Vue component, use client:only="vue". Props work as they do with other directives and must be serialisable, because they're still sent to the browser in the island's props attribute.
FAQ: Astro client:only
No. Astro doesn't run the component on the server at all. The only server-rendered content is the island wrapper and any fallback content you pass with slot="fallback".
Astro normally finds the right renderer by rendering the component on the server. With client:only it never does, so you must tell it which framework's client code to load.
It can be. Search engines that don't run JavaScript only see the fallback content. Keep important text in server-rendered HTML and use client:only for widgets that don't need to be indexed.
Immediately on page load, like client:load. There's no lazy variant, so if a browser-only component is far down the page, consider keeping server rendering and using client:visible with browser code in an effect.
No. Astro components never run in the browser. For browser-only behaviour in an Astro component, add a script tag instead.
Pass a fallback that takes up the same space as the final component, such as a skeleton with a fixed height, or reserve the space with CSS on a wrapping element.
Conclusion
client:only tells Astro to leave a framework component out of server rendering and build it entirely in the browser. Write it with the framework's name, such as client:only="react" or client:only="svelte", and add a slot="fallback" child so visitors see something useful while it loads. Astro will catch a missing framework name at build time, but not a wrong one, so check it carefully.
Use it for components that genuinely depend on the browser during render, like those reading localStorage or wrapping DOM-bound libraries. For everything else, prefer keeping server rendering and moving browser code into effects, loading heavy libraries lazily, or using plain scripts and server islands. That way you keep the fast, indexable HTML that makes Astro pages worth building in the first place.


