Passing Server Components as children into a Client Component is the way to keep server-rendered content inside an interactive wrapper without shipping that content's code to the browser. The Client Component never imports the Server Component. A Server Component higher in the tree renders it first, then hands the Client Component the finished output as a normal React element.
// app/ui/expandable-panel.tsx
'use client'
import { useState } from 'react'
export function ExpandablePanel({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false)
return (
<section>
<button onClick={() => setOpen(!open)}>
{open ? 'Hide comments' : 'Show comments'}
</button>
{open && children}
</section>
)
}The panel component runs on the client. It needs the use client directive because it holds open and closed state with useState and responds to a button click, neither of which a Server Component can do. It only knows about a children slot, not what fills that slot.
// app/tickets/[id]/page.tsx
import { ExpandablePanel } from '@/app/ui/expandable-panel'
import { getComments } from '@/lib/data'
type Params = Promise<{ id: string }>
export default async function TicketPage({ params }: { params: Params }) {
const { id } = await params
const comments = await getComments(id)
return (
<ExpandablePanel>
<p>{comments.length} comments loaded from the server</p>
</ExpandablePanel>
)
}The ticket page is a Server Component, so it runs only on the server and reads the comments straight out of the database. Clicking the panel's button reveals content that was already fully rendered on the server, with no fetch request shipped to the browser at all.
Why the comment count stays on the server
The use client directive marks a boundary in the module graph, and that boundary only covers what a file imports and renders directly. The panel component's file never imports the comment data's code. It only receives a children prop, which is just data describing already-rendered output, so that code never becomes part of the client bundle.
Render order for a request to /tickets/42:
1. TicketPage runs on the server and awaits getComments()
2. The comment count renders to the RSC payload on the server
3. The panel component (client) receives that output as children
4. The browser hydrates the panel, not the comment count's sourceThe database call inside getComments, and everything that reads its result, never crosses into client JavaScript. Only its rendered markup travels down, the same way server-rendered HTML would travel to any browser.
This is the real difference between composition through a prop and an ordinary import. An import drags a whole module along with it, while a prop only carries the result that module already produced. That distinction is what lets an interactive shell sit visually inside the same tree as server-only code, without ever pulling that code into the bundle the browser downloads.
Passing through a named prop instead of children
The pattern is not limited to the children prop. Any prop position works the same way, as long as the value you pass is a rendered Server Component, not a plain reference to the component function.
// app/ui/side-drawer.tsx
'use client'
import { useState } from 'react'
export function SideDrawer({ footer }: { footer: React.ReactNode }) {
const [open, setOpen] = useState(false)
return (
<aside>
<button onClick={() => setOpen(!open)}>Toggle drawer</button>
{open && footer}
</aside>
)
}The drawer component runs on the client for the same reason the panel does, since it owns local open and closed state. Its footer prop is just a named slot instead of children, and a parent Server Component can fill that slot the same way it fills a children slot.
// app/dashboard/page.tsx
import { SideDrawer } from '@/app/ui/side-drawer'
import { AccountSummary } from '@/app/ui/account-summary'
export default function DashboardPage() {
return <SideDrawer footer={<AccountSummary />} />
}The dashboard page and the account summary component are both Server Components. Passing the rendered account summary into the footer prop renders it on the server and hands the drawer its output, the same composition that children gives you, just through a different prop name and a different visual slot in the layout.
What the Client Component can and cannot do with it
Once a Client Component receives a Server Component this way, treat it as an opaque node. It can place it, wrap it in markup, or show and hide it with local state, exactly what the panel and drawer components do above with their own open and closed state.
What it cannot reliably do is reach into that node and change it. Calling React.cloneElement to inject new props onto a passed-in Server Component is unsupported for an async Server Component in current React, and even outside that case the React documentation recommends treating any created element as read-only once it exists. Do not design a pattern around cloning or reading the internals of children a Client Component receives this way, since that is not what the composition model promises to support.
A practical use case
Reach for this pattern whenever an interactive shell, such as an accordion, a drawer, or a modal, needs to wrap content that still requires server data. The wrapper owns the open and closed state, while every data-heavy piece stays a Server Component passed in through a prop instead of being imported into the wrapper's own file.
Keep the wrapper file as the only Client Component in that section of the tree. Every server-fetched piece passed into it, whether through children or a named prop like footer, keeps its data fetching and any secrets on the server, and none of it adds JavaScript of its own to the page the browser downloads.
Common mistakes
- Importing the Server Component directly inside the Client Component's file instead of passing it in from a parent, which fails to build because that import pulls server-only code into the client module graph.
- Passing a plain reference to the component function instead of a rendered element, which the Client Component cannot place the way it places ordinary children.
- Trying to clone or read props off a Server Component element inside the Client Component, which is unreliable and unsupported for async Server Components.
- Marking the wrapper's parent page as a Client Component too, which removes the Server Component that was doing the actual rendering of the content being passed down.
For the underlying boundary rules this pattern relies on, see Next.js Server vs Client Boundary Explained. For the full difference between the two component types, see Server Components vs Client Components in Next.js. For ordinary data props instead of composition, see How to Pass Props Across the Server/Client Boundary, and for a wider survey of composition patterns beyond this one, see Composition Patterns for Server and Client Components.
Rune AI
Key Insights
- A Client Component cannot import a Server Component, but a parent Server Component can pass one in as children or another prop.
- The Server Component renders on the server first, then arrives at the Client Component as already-rendered output, not source code.
- This works for any prop position, not only children, as long as you pass a rendered element rather than a function reference.
- The receiving Client Component must treat the passed element as opaque, since it cannot read or reliably clone its internals.
- This is a composition pattern, separate from passing serializable data as props.
- Use it whenever an interactive wrapper, like a panel or modal, needs to surround server-fetched content.
Frequently Asked Questions
Does passing a Server Component as children send its source code to the browser?
Does this only work with the children prop?
Can the Client Component inspect or change what the Server Component rendered?
Why does this fail if I mark the wrapper as a Client Component and still try to import the Server Component directly?
Conclusion
Passing a Server Component into a Client Component as children or another prop keeps that content rendering on the server, because the Client Component never imports it, it only receives the finished output. Use this whenever an interactive wrapper needs to surround server-rendered content instead of importing it directly.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.