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.
- 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, reading its document from config:
'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:
'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:
import {ClientApp} from './ClientApp'
export default function Page() {
return (
<main>
<ClientApp />
</main>
)
}App is the one from Get started, with the project and dataset read from config:
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:
// 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:
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=postThe 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.
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:
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:
import type {NextConfig} from 'next'
const nextConfig: NextConfig = {
compiler: {styledComponents: true},
}
export default nextConfigAdd 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 is the authority if it changes:
'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:
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 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.
- Why the package takes the address rather than reading it: Comment navigation and deep links.
- A comments panel in the App Router, as an example you can run: nextjs.
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?