THEME
MOTION
HAPTIC
SOUND

NEXT.JS

Cathode UI works out of the box in Next.js — App Router, Pages Router, Remix, and React Router 7. Every primitive ships with a "use client" directive baked in, so you do not need to wrap CathodeProvider or individual components to satisfy React Server Components.

TL;DR

Cathode primitives are client components. They use hooks (useState, useEffect), portals, motion, and browser-only APIs (navigator.vibrate, Web Audio), so they cannot execute on the server. The package declares itself as a client boundary for you.

npm install @cathode-ui/react

App Router setup

Put the CSS imports in your root layout.tsx. Wrap the tree in CathodeProvider from a small client component — the layout itself stays a Server Component (good for streaming and metadata).

1. Client wrapper (app/providers.tsx):

'use client';

import type { ReactNode } from 'react';
import { CathodeProvider } from '@cathode-ui/react';

export function Providers({ children }: { children: ReactNode }) {
  return <CathodeProvider>{children}</CathodeProvider>;
}

2. Root layout (app/layout.tsx):

import '@cathode-ui/react/tokens.css';
import '@cathode-ui/react/fonts.css';
import '@cathode-ui/react/styles.css';

import { Providers } from './providers';

export const metadata = { title: 'My app' };

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

3. Use primitives anywhere (Server or Client pages):

// app/page.tsx — can stay a Server Component.
import { Button, TerminalFrame, PulsingDot } from '@cathode-ui/react';

export default function Home() {
  return (
    <TerminalFrame title="STATUS">
      <PulsingDot color="var(--cathode-color-success)" />
      <Button variant="primary">SUBMIT</Button>
    </TerminalFrame>
  );
}

That works because every Cathode export is a client component — Next crosses the client boundary at the @cathode-ui/react import site, not at your page. You can render Cathode primitives directly from Server Components without 'use client' on the page file.

Pages Router setup

The Pages Router has no RSC boundary, so the setup is identical to a standard React app. Mount the provider in pages/_app.tsx:

import type { AppProps } from 'next/app';
import '@cathode-ui/react/tokens.css';
import '@cathode-ui/react/fonts.css';
import '@cathode-ui/react/styles.css';
import { CathodeProvider } from '@cathode-ui/react';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <CathodeProvider>
      <Component {...pageProps} />
    </CathodeProvider>
  );
}

Using with RSC

You can import a Cathode primitive inside a Server Component — Next's compiler sees the "use client" directive on the Cathode module and crosses the boundary automatically. The props you pass must be serializable (strings, numbers, plain objects) because they cross the server → client boundary.

Do not pass server-only values — functions from Server Actions are fine, but raw file handles, DB clients, etc. will fail serialization. This is a general RSC rule, not Cathode-specific.

Remix + React Router 7

Both frameworks adopt the same client-boundary model as Next App Router. The root app/root.tsx stays a Server Component; wrap the outlet in a client-side Providers component as shown above. No Cathode-specific configuration required.

Troubleshooting

Error: "You're importing a component that needs useState / useEffect …"

If you see this when using Cathode inside an App Router page, upgrade @cathode-ui/react to 0.4.0 or later — earlier versions did not ship the "use client" directive in the bundle.

npm install @cathode-ui/react@latest

Error: "Functions cannot be passed directly to Client Components"

You're passing a non-serializable prop (a closure, class instance, or Date/Map/Set) from a Server Component into a Cathode primitive. Move the prop into a local client component, or serialize it before the boundary.

Fonts not loading

@cathode-ui/react/fonts.css ships JetBrains Mono via @font-face — Next will tree-shake it if you don't import it from a client module. Keep the import in app/layout.tsx (as shown above) so it lands in the global CSS stream.


See also: Getting Started, all components.