Using cascivo with Next.js (App Router / RSC)
cascivo works in Next.js App Router projects out of the box: components ship
with 'use client' preserved in the published bundle, so React Server
Components treat them as client components without any wrapper on your side.
This page covers the wiring and how the server/client split falls out.
Prerequisite reading: GETTING-STARTED.md for the two
install paths. Everything below applies to both; snippets use the prebuilt
@cascivo/react package.
Install
pnpm add @cascivo/react @cascivo/themes @preact/signals-react
Import the themes CSS in the root layout
Do this once, in a Server Component — app/layout.tsx is the natural place.
Next.js supports global CSS imports in Server Components, and this keeps the
theme out of every client bundle:
// app/layout.tsx — a Server Component (no 'use client')
import '@cascivo/themes/light-dark.css' // tokens (once) + base typography + light & dark
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" data-theme="light">
<body>{children}</body>
</html>
)
}
data-theme on <html> themes the whole app; it can also scope any subtree
(<aside data-theme="dark">). See THEMING.md.
Theme switching without a flash (SSR)
The static data-theme="light" above is a fine default. For a user-toggleable theme
that survives reload with no flash of the wrong theme, use the theme runtime from
@cascivo/react: inline themePreloadScript() in the root layout so the persisted theme
paints on the first byte, then toggle from a client component with useTheme().
// app/layout.tsx — Server Component. The pre-paint script sets data-theme before the
// app bundle runs, so there is no flash; the client then owns toggling.
import '@cascivo/themes/light-dark.css'
import { themePreloadScript } from '@cascivo/react'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<script dangerouslySetInnerHTML={{ __html: themePreloadScript() }} />
</head>
<body>{children}</body>
</html>
)
}
Wrap your client tree in <ThemeProvider> and toggle with useTheme() (a 'use client'
component). Full API in THEMING.md.
Component CSS is bundled automatically
Each component pulls in its own stylesheet when you import it — Next.js (webpack or Turbopack) includes the styles only for the components you actually use and tree-shakes the rest. There is no component-CSS import to add or maintain; the themes import above is the only global CSS wiring.
This holds for Server Components as well as client ones. RSC resolves a dependency
with the react-server export condition, which @cascivo/react points at its
CSS-bearing build; the CSS-free node/ twin stays reserved for bare-Node SSR loaders
that cannot import a .css file at all.
Fixed in 0.18.0. Before that release there was no
react-servercondition, so RSC fell through tonodeand every component without a'use client'directive — exactly theclientJs: 'none'ones — rendered on the server from a build with no CSS edges. Their stylesheets were never collected and they painted unstyled. The documented workaround was to import the 328 KB aggregate@cascivo/react/styles.cssin the root layout, which is why an SSR page shipped ~384 KB of CSS. If you are still doing that, delete the import: measured onapps/examples/react-next, the same page drops to 34 KB.test/rsc-css.mjsasserts every class in the prerendered HTML has a matching rule, so this cannot regress silently again.
How the server/client split works
- Every cascivo component is a client component. Interactivity is driven by
Preact signals, which run in the browser;
'use client'is preserved in the@cascivo/reactbundle, so each component import creates its own client boundary automatically. - You can render them directly from Server Components. A Server Component
page can emit
<Button>or<Card>in its JSX; Next.js serializes the props across the boundary. Server-rendered HTML is produced as usual, then the component hydrates on the client. - Props crossing the boundary must be serializable. Passing
childrenand plain data is fine; passing event handlers (onClick,onChange) from a Server Component is not — Next.js will error. Put interactive composition (handlers, signals, form state) inside your own'use client'file:
// app/page.tsx — Server Component: static composition is fine
import { Card, CardContent, Badge } from '@cascivo/react'
import { SaveButton } from './save-button'
export default function Page() {
return (
<Card>
<CardContent>
<Badge>Beta</Badge>
<SaveButton />
</CardContent>
</Card>
)
}
// app/save-button.tsx — your client boundary for interactivity
'use client'
import { Button } from '@cascivo/react'
export function SaveButton() {
return <Button onClick={() => save()}>Save</Button>
}
If your own client component reads a signal's .value during render, call
useSignals() (from @cascivo/core) as its first statement — see the gotcha in
TESTING.md and
TROUBLESHOOTING.md.
Copy-paste flow in Next.js
npx cascivo init + npx cascivo add <component> work the same in a Next.js
repo: source is copied to outputDir (default src/components/ui) with
'use client' already in the source files. Import from there instead of
@cascivo/react; the theming and boundary rules above are identical.
Client-side routing for nav components
cascivo's config-driven nav (SideNav, ShellHeader, Header, Breadcrumb, …) renders
plain <a href> by default. To route those links through Next.js <Link> — keeping
client-side transitions and prefetching — register the link component once at app start
(a module singleton; no provider). Do it in a small client component mounted in your root
layout:
'use client'
import { setLinkComponent } from '@cascivo/react'
import Link from 'next/link'
// next/link already takes `href`, so the cascivo prop bag maps 1:1.
setLinkComponent(Link)
Import setLinkComponent (and the LinkComponentProps type, if you write a custom adapter)
from @cascivo/react, not @cascivo/core — see the phantom-dependency note in
HEADLESS.md. After this, every
href-based nav item navigates via the router and inherits its prefetching.
Naming collisions with Next.js globals
Two cascivo exports shadow Next.js modules — alias them on import:
import { Image as CascivoImage, Link as CascivoLink } from '@cascivo/react'
Editor auto-import sometimes picks the wrong Image/Link; if styles or
routing break after adding one, check the import resolves where you meant. Full
list in the @cascivo/react README.
FAQ
Can I use cascivo with React Server Components at all, given it's
signal-driven? Yes. Components are RSC-compatible and mark themselves
'use client' where they need interactivity — the signals runtime only ever
executes in the browser. Server Components compose them freely as long as the
props they pass are serializable.
Where's a working example? apps/examples/react-next
is the Next.js App Router example in this repo, demonstrating 'use client'
boundary placement for signal-driven components.
See also
- COMPATIBILITY.md — framework and browser matrix.
- GETTING-STARTED.md — install paths and the theme wiring.
- CSS-LAYERS-PITFALL.md — before adding a global reset
to
globals.css.
As Markdown: /docs/using-with-nextjs.md