VisorVisor

SSR Compatibility

Which Visor components are server-safe vs client-only, and how to use them correctly in Next.js App Router.

Overview

Visor components are distributed as copy-and-own source files. When you add them to a Next.js App Router project, some components render on the server without issue, while others require the React Client Component boundary ("use client").

This page documents the SSR status of every component and hook, explains why each classification exists, and shows how to use client components correctly within a server-first architecture.

Component SSR Matrix

ComponentSSR StatusReason
AlertServer-safePure HTML rendering, no hooks, no Radix
AvatarClient-onlyRadix UI primitive (@radix-ui/react-avatar)
BadgeServer-safePure HTML rendering, no hooks, no Radix
BreadcrumbServer-safePure HTML rendering, no hooks, no Radix
ButtonServer-safePure HTML rendering, no hooks, no Radix
CardServer-safePure HTML rendering, no hooks, no Radix
ChartClient-onlyUses useContext, useMemo, and Recharts (client-only charting library)
CheckboxClient-onlyRadix UI primitive (@radix-ui/react-checkbox)
DialogClient-onlyRadix UI primitive (@radix-ui/react-dialog), manages open/close state
DropdownMenuClient-onlyRadix UI primitive (@radix-ui/react-dropdown-menu), manages open/close state
FieldServer-safePure HTML rendering, no hooks, no Radix
InputServer-safePure HTML rendering, no hooks, no Radix
LabelClient-onlyRadix UI primitive (@radix-ui/react-label)
ProgressClient-onlyRadix UI primitive (@radix-ui/react-progress)
ScrollAreaClient-onlyRadix UI primitive (@radix-ui/react-scroll-area)
SelectClient-onlyRadix UI primitive (@radix-ui/react-select), manages open/close state
SeparatorClient-onlyRadix UI primitive (@radix-ui/react-separator)
SheetClient-onlyRadix UI primitive (@radix-ui/react-dialog), manages open/close state
SidebarClient-onlyUses useState, useEffect, useContext, useRef, media query hooks
SkeletonServer-safePure HTML rendering, no hooks, no Radix
SwitchClient-onlyRadix UI primitive (@radix-ui/react-switch)
TabsClient-onlyRadix UI primitive (@radix-ui/react-tabs), manages active tab state
TextareaServer-safePure HTML rendering, no hooks, no Radix
TooltipClient-onlyRadix UI primitive (@radix-ui/react-tooltip), manages open/close state

Hook SSR Matrix

All custom hooks require the client boundary because they use React hooks (useState, useEffect, useRef, etc.) which only run in the browser.

HookSSR StatusReason
useBooleanClient-onlyUses useState, useCallback
useClickOutsideClient-onlyUses useEffect, accesses document
useDebounceClient-onlyUses useState, useEffect
useFocusTrapClient-onlyUses useEffect, accesses document
useIntersectionObserverClient-onlyUses useState, useEffect, useRef, IntersectionObserver API
useKeyboardShortcutClient-onlyUses useEffect, accesses document
useLocalStorageClient-onlyUses useState, useEffect, accesses window.localStorage
useMediaQueryClient-onlyUses useState, useEffect, accesses window.matchMedia
usePreviousClient-onlyUses useEffect, useRef

Why Radix UI Requires "use client"

All Radix UI primitives use React hooks internally for state management, focus trapping, keyboard navigation, and portal rendering. Any component that imports from a @radix-ui/* package must be a Client Component — even if your wrapper doesn't call any hooks directly.

Usage Patterns

Server-safe components

Server-safe components can be imported and rendered directly in Server Components — no wrapper needed.

// app/page.tsx (Server Component — no "use client")
import { Alert } from "@/components/ui/alert/alert"
import { Badge } from "@/components/ui/badge/badge"
import { Button } from "@/components/ui/button/button"
import { Card } from "@/components/ui/card/card"

export default function Page() {
  return (
    <Card>
      <Alert variant="info">This renders on the server.</Alert>
      <Badge>New</Badge>
      <Button>Get Started</Button>
    </Card>
  )
}

Client components in Server Components

Client components can be children of Server Components. The server component passes data down; the client component handles interactivity.

// app/page.tsx (Server Component)
import { UserMenu } from "@/components/user-menu" // client component

export default async function Page() {
  const user = await fetchUser() // server-side data fetch
  return <UserMenu user={user} />
}
// components/user-menu.tsx (Client Component)
"use client"

import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu/dropdown-menu"

export function UserMenu({ user }: { user: User }) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger>{user.name}</DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuItem>Profile</DropdownMenuItem>
        <DropdownMenuItem>Sign out</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Passing Server Component children into Client Components

You can pass Server Component subtrees as children into client components. React renders the server children on the server and slots them into the client component.

// app/layout.tsx (Server Component)
import { Sidebar, SidebarProvider } from "@/components/ui/sidebar/sidebar"
import { ServerNav } from "@/components/server-nav" // Server Component

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <Sidebar>
        <ServerNav /> {/* rendered on server, passed as children */}
      </Sidebar>
      <main>{children}</main>
    </SidebarProvider>
  )
}

Lazy loading client components

For heavy client components (like Chart or Dialog), use next/dynamic to defer loading until needed.

import dynamic from "next/dynamic"

const Chart = dynamic(
  () => import("@/components/ui/chart/chart").then((m) => m.ChartContainer),
  {
    loading: () => <div>Loading chart...</div>,
    ssr: false, // skip server render for pure client-side charts
  }
)

Best Practices

  1. Keep Server Components at the leaves — fetch data in Server Components and pass it as props to Client Components. This maximizes server rendering.

  2. Push "use client" boundaries down — only mark the smallest subtree that actually needs interactivity as a Client Component. Don't add "use client" to layout or page files unless required.

  3. Compose, don't wrap — pass server-rendered children into client components via children instead of importing server components inside client components.

  4. Avoid ssr: false unless necessarynext/dynamic with ssr: false skips the initial HTML entirely. Only use it for components that depend on browser-only APIs unavailable in Node.js (e.g., WebGL, canvas).