> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# Use comments in Next.js

Render the comments panel in the Next.js App Router: the client boundary, the stylesheet import, and the styled-components setup.

The comments panel is the same in Next.js as in any React app. What changes is where the client boundary goes, where the stylesheet is imported, and what `styled-components` needs from the App Router. This guide covers those three things, so that a panel you have already built renders correctly in the App Router.

Prerequisites:

- A working comments panel. See [Get started](https://www.sanity.io/docs/collaboration/get-started).
- A Next.js app on the App Router, running Next.js 16 or later. With TypeScript 7, you need Next.js 16.3.0 or later; on earlier 16.x releases, use TypeScript 5.

## Mark the client boundary once

The components use context, refs, and an editor, and the SDK hooks subscribe. None of that runs on the server, so the panel is a client component.

Mark the boundary once, as high as the panel. This is the `Comments.tsx` from [Get started](https://www.sanity.io/docs/collaboration/get-started), reading its document from `config`:

**app/Comments.tsx**

```tsx
'use client'

import {SDKCommentsProvider, SDKCommentsPanel} from '@sanity/collaboration/sdk'
import {Suspense} from 'react'

import {config} from './config'

export function Comments() {
  return (
    <Suspense fallback={<p>Resolving your identity…</p>}>
      <SDKCommentsProvider>
        <Suspense fallback={<p>Loading comments…</p>}>
          <SDKCommentsPanel
            documentId={config.documentId}
            documentType={config.documentType}
            compose={{fieldPath: 'title'}}
          />
        </Suspense>
      </SDKCommentsProvider>
    </Suspense>
  )
}
```

`'use client'` doesn't stop Next.js from prerendering the component on the server, and `SanityApp` reads `window` while it renders, so `next build` fails with `window is not defined`. Load `App` in the browser only, with `next/dynamic` and `ssr: false`. Next.js only allows `ssr: false` in a client component, so it goes in a small wrapper:

**app/ClientApp.tsx**

```tsx
'use client'

import dynamic from 'next/dynamic'

// `SanityApp` reads `window` while it renders, so load the SDK tree in the
// browser only.
export const ClientApp = dynamic(() => import('./App').then((m) => m.App), {
  ssr: false,
  loading: () => <p>Starting…</p>,
})
```

The page stays a server component and renders the wrapper:

**app/page.tsx**

```tsx
import {ClientApp} from './ClientApp'

export default function Page() {
  return (
    <main>
      <ClientApp />
    </main>
  )
}
```

`App` is the one from [Get started](https://www.sanity.io/docs/collaboration/get-started), with the project and dataset read from `config`:

**app/App.tsx**

```tsx
import {AuthBoundary, SanityApp} from '@sanity/sdk-react'
import {ThemeProvider} from '@sanity/ui'
import {buildTheme} from '@sanity/ui/theme'

import {Comments} from './Comments'
import {config} from './config'

export function App() {
  return (
    <ThemeProvider theme={buildTheme()}>
      <SanityApp
        config={{
          projectId: config.projectId,
          dataset: config.dataset,
        }}
        fallback={<p>Starting…</p>}
      >
        <AuthBoundary>
          <Comments />
        </AuthBoundary>
      </SanityApp>
    </ThemeProvider>
  )
}
```

Server components can render around the wrapper, including the page, the document body, and the layout. They can't be imported into a client component.

Next.js only exposes environment variables prefixed with `NEXT_PUBLIC_` to the browser, and only inlines `process.env` references it can see written out in full:

**app/config.ts**

```typescript
// Next.js only inlines `process.env.NEXT_PUBLIC_*` references it can see
// literally, so each variable is written out in full.
function required(name: string, value: string | undefined): string {
  if (!value) throw new Error(`Set ${name} in .env and restart.`)
  return value
}

export const config = {
  projectId: required(
    'NEXT_PUBLIC_SANITY_PROJECT_ID',
    process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
  ),
  dataset: required(
    'NEXT_PUBLIC_SANITY_DATASET',
    process.env.NEXT_PUBLIC_SANITY_DATASET,
  ),
  documentId: required(
    'NEXT_PUBLIC_SANITY_DOCUMENT_ID',
    process.env.NEXT_PUBLIC_SANITY_DOCUMENT_ID,
  ),
  documentType: required(
    'NEXT_PUBLIC_SANITY_DOCUMENT_TYPE',
    process.env.NEXT_PUBLIC_SANITY_DOCUMENT_TYPE,
  ),
}
```

Set them in `.env`:

**.env**

```sh
NEXT_PUBLIC_SANITY_PROJECT_ID=YOUR_PROJECT_ID
NEXT_PUBLIC_SANITY_DATASET=production
NEXT_PUBLIC_SANITY_DOCUMENT_ID=post-123
NEXT_PUBLIC_SANITY_DOCUMENT_TYPE=post
```

The Next.js development server runs on `http://localhost:3000`. Add that origin, with credentials allowed, to the project's CORS origins at [sanity.io/manage](https://www.sanity.io/manage).

## Import the stylesheet in the root layout

Sanity UI ships its static styles as a plain stylesheet, and the root layout is where the App Router expects global CSS:

**app/layout.tsx**

```tsx
import '@sanity/ui/styles.css'

// ...
```

## Enable the styled-components compiler

`styled-components` needs Next.js's compiler support turned on. Without it you get hydration mismatches on anything the components style:

**next.config.ts**

```typescript
import type {NextConfig} from 'next'

const nextConfig: NextConfig = {
  compiler: {styledComponents: true},
}

export default nextConfig
```

## Add a styled-components registry

`styled-components` also needs a style registry in the App Router, to collect styles during the server render and flush them into the document head. Without one, the comment styles flash unstyled on first paint.

This is Next.js's documented pattern, reproduced here so the panel setup is in one place. [How to use CSS-in-JS libraries](https://nextjs.org/docs/app/guides/css-in-js) is the authority if it changes:

**app/StyledComponentsRegistry.tsx**

```tsx
'use client'

import {useServerInsertedHTML} from 'next/navigation'
import {useState} from 'react'
import {ServerStyleSheet, StyleSheetManager} from 'styled-components'

export function StyledComponentsRegistry({
  children,
}: {
  children: React.ReactNode
}) {
  const [sheet] = useState(() => new ServerStyleSheet())

  useServerInsertedHTML(() => {
    const styles = sheet.getStyleElement()
    sheet.instance.clearTag()
    return <>{styles}</>
  })

  if (typeof window !== 'undefined') return <>{children}</>

  return (
    <StyleSheetManager sheet={sheet.instance}>{children}</StyleSheetManager>
  )
}
```

Wrap the body's children in it:

**app/layout.tsx**

```tsx
import '@sanity/ui/styles.css'

import {StyledComponentsRegistry} from './StyledComponentsRegistry'

export default function RootLayout({children}: {children: React.ReactNode}) {
  return (
    <html lang="en">
      <body>
        <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
      </body>
    </html>
  )
}
```

If your app already has a registry, the panel needs nothing further.

## Render on the server

There is no `window` during a server render, which is one reason the package never reads the URL itself. The address is passed in as a value, so it is available on the first client render without an effect. See [Link to a comment](https://www.sanity.io/docs/collaboration/link-to-a-comment) for the App Router wiring, including the `{scroll: false}` that keeps the router from fighting the scroll the package performs.

## Next steps

- Deep links in the App Router: [Link to a comment](https://www.sanity.io/docs/collaboration/link-to-a-comment).
- Why the package takes the address rather than reading it: [Comment navigation and deep links](https://www.sanity.io/docs/collaboration/navigation).
- A comments panel in the App Router, as an example you can run: [nextjs](https://github.com/sanity-io/collaboration-platform-examples/tree/main/examples/nextjs).

