Skip to Content
ComponentsClientOnly

ClientOnly

ClientOnly renders deterministic fallback UI during server rendering and the first hydration pass, then renders browser-only children after the component has mounted in the browser.

Use it when a subtree depends on APIs such as window, document, storage, media queries, observers, clipboard, geolocation, charts, maps, or widgets that cannot safely render during SSR.

Live Example

Client-only rendering patterns

Fallbacks, delays, feature requirements, render strategies, and hydration-safe browser state.

Basic ClientOnly

Preparing client UI...

Fallback

Loading the browser-only panel...

Delay

Delaying for 900ms...

Function children

Waiting for mount...

Requires localStorage

Checking storage support...

Requires matchMedia

Checking media query support...

Unsupported fallback

Checking optional browser APIs...

Idle strategy

Waiting for idle time...

Animation frame strategy

Waiting for the next frame...

Hydration-safe browser width

Measuring after hydration...

Import

"use client"; import { ClientOnly } from "react-rsc-kit/client";

ClientOnly is exported from react-rsc-kit/client. It is not exported from react-rsc-kit or react-rsc-kit/server, so server component entrypoints stay browser-free.

Why It Exists

Server-rendered React must produce HTML that matches the first client render. Reading browser-only values during render can create hydration mismatches:

// Avoid this during SSR. const width = window.innerWidth;

ClientOnly avoids that by rendering fallback on the server and on the first client render. Browser-only children are evaluated only after mount.

Basic Usage

"use client"; import { ClientOnly } from "react-rsc-kit/client"; export function ChartPanel() { return ( <ClientOnly> <Chart /> </ClientOnly> ); }

Fallback Usage

<ClientOnly fallback={<div aria-busy="true">Loading chart...</div>}> <Chart /> </ClientOnly>

If fallback is omitted, ClientOnly renders null before the browser subtree is ready. fallback={null} is also supported.

Function Children

<ClientOnly fallback={<Skeleton />}> {({ isClient, isReady, isSupported, missingFeatures }) => isClient && isReady && isSupported ? ( <Chart /> ) : ( <Skeleton label={`Missing: ${missingFeatures.join(", ")}`} /> ) } </ClientOnly>

Render-prop children receive:

KeyTypeDescription
isClientbooleanWhether the component has mounted.
isReadybooleanWhether delay and strategy work completed.
isSupportedbooleanWhether required features are available.
missingFeaturesMissingClientFeature[]Required features that were not detected.

Delay Usage

<ClientOnly fallback={<Skeleton />} delay={300}> <ExpensiveWidget /> </ClientOnly>

delay is measured in milliseconds. Negative delay values are treated as 0, and timers are cleaned up on unmount.

Browser Feature Requirements

<ClientOnly fallback={<Skeleton />} unsupportedFallback={<UnsupportedBrowser />} require={{ window: true, document: true, localStorage: true, sessionStorage: false, matchMedia: true, intersectionObserver: false, resizeObserver: false, clipboard: false, geolocation: false, }} > <BrowserOnlyComponent /> </ClientOnly>

Only features set to true are required. Storage checks use guarded reads and a write/remove probe because browser storage can exist but still throw.

Unsupported Browser Fallback

<ClientOnly fallback={<Skeleton />} unsupportedFallback={(missingFeatures) => ( <p>Your browser is missing: {missingFeatures.join(", ")}</p> )} require={{ localStorage: true, matchMedia: true }} > <PreferencesPanel /> </ClientOnly>

When a required feature is missing, unsupportedFallback renders. If it is omitted, fallback renders instead. onUnsupported receives the missing feature list once per missing feature set.

Strategy Options

StrategyBehavior
effectDefault. Marks ready from a React effect after mount.
idleUses requestIdleCallback, with setTimeout fallback.
animation-frameUses requestAnimationFrame, with setTimeout fallback.
<ClientOnly fallback={<Skeleton />} strategy="idle"> <AnalyticsHeavyPanel /> </ClientOnly>

All scheduler handles are cleaned up on unmount.

Lifecycle Callbacks

<ClientOnly onReady={() => logReady()} onUnsupported={(missing) => reportMissingFeatures(missing)} onError={(error) => reportClientOnlyError(error)} > <Widget /> </ClientOnly>

onReady and onUnsupported are guarded for React Strict Mode so development double-effect checks do not duplicate calls.

Next.js App Router And RSC

Use ClientOnly inside a file with a client boundary:

"use client"; import { ClientOnly } from "react-rsc-kit/client"; export function MapClientBoundary() { return ( <ClientOnly fallback={<div aria-busy="true">Loading map...</div>}> <MapWidget /> </ClientOnly> ); }

Server components can render this client component as usual, but should not import ClientOnly directly from a server file.

Testing

import { act, render, screen } from "@testing-library/react"; import { vi } from "vitest"; import { ClientOnly } from "react-rsc-kit/client"; it("waits for delay", () => { vi.useFakeTimers(); render( <ClientOnly fallback={<span>Loading</span>} delay={300}> <span>Ready</span> </ClientOnly>, ); expect(screen.getByText("Loading")).toBeInTheDocument(); act(() => { vi.advanceTimersByTime(300); }); expect(screen.getByText("Ready")).toBeInTheDocument(); });

For SSR tests, use renderToString and assert that fallback HTML is present while browser-only children are absent.

Accessibility

ClientOnly does not add ARIA roles or labels. Put accessible loading states in your fallback:

<ClientOnly fallback={<div aria-busy="true">Loading chart...</div>}> <Chart /> </ClientOnly>

For unsupported browsers, explain what is missing and provide a recovery path when possible.

Troubleshooting

  • Import from react-rsc-kit/client, not from react-rsc-kit/server.
  • Keep browser globals out of parent render code; put them inside children rendered after ClientOnly is ready.
  • Use require when a child depends on optional browser APIs.
  • Use unsupportedFallback when feature requirements can fail in privacy-restricted or older browsers.
  • Use fake timers for delay, idle fallback, and animation-frame tests.
  • Prefer stable require objects in large trees, although inline objects are supported.
Last updated on