SSR & Next.js
NeumorUI is built for server-rendered apps. Hydration-safe IDs, no window access at render-time, and zero useLayoutEffect warnings (as of v0.5.0).
Next.js App Router setup
Wrap your app once at the root. NeuProvider is a Client Component, so you need a wrapper if your layout.tsx is a Server Component.
1. Create a Providers wrapper
// app/providers.tsx
"use client";
import { NeuProvider } from "neumorui";
export function Providers({ children }: { children: React.ReactNode }) {
return <NeuProvider followSystemTheme>{children}</NeuProvider>;
}2. Use it from your root layout
// app/layout.tsx
import "neumorui/styles";
import { Providers } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Which components need "use client"?
Components with state, refs, or event handlers must run on the client. That includes most NeumorUI components: Button (ripple), Modal, Drawer, Tooltip, anything interactive. If you use them from a Server Component, wrap them in a Client Component.
// app/page.tsx — Server Component, OK to use NeuProvider's static children
import { Card } from "neumorui";
export default function HomePage() {
return (
<Card>
<h1>Welcome</h1>
<p>Static content renders on the server.</p>
</Card>
);
}// app/dashboard/interactive.tsx — Client Component for interactive bits
"use client";
import { Button, useToast } from "neumorui";
export function SaveButton() {
const { toast } = useToast();
return <Button onClick={() => toast({ message: "Saved" })}>Save</Button>;
}Hydration safety
NeumorUI follows React 18 SSR best practices end-to-end:
- Item IDs use
useId()+ a ref counter — stable across server and client (noMath.random()) - All
window/document/localStorageaccess happens insideuseEffector event handlers localStoragetheme restore runs after mount (initial render matches server)- No
useLayoutEffectwarnings on the server - TypeScript
strictmode passes
Tip: If you see a hydration warning, it's likely from your own code (e.g. using Date.now() at render). Check the browser console — React tells you which DOM mismatch caused it.
Avoiding the dark-mode flash
If you use followSystemTheme or restore from localStorage, the server renders "light" first and the browser briefly flashes light before switching to the saved theme. Two ways to fix this:
Option A — Set data-theme early via inline script
// app/layout.tsx
<html lang="en">
<head>
<script
dangerouslySetInnerHTML={{
__html: `(function () {
var saved = localStorage.getItem('neu-theme');
var dark = saved === 'dark' || (!saved && matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
})()`,
}}
/>
</head>
<body>{children}</body>
</html>Option B — Pick a default and skip system detection
<NeuProvider defaultTheme="dark"> {/* always dark, no flash */}
{children}
</NeuProvider>Vite / Vercel / Remix
NeumorUI works the same way on Vite SSR, Remix, Astro islands, etc. Just import "neumorui/styles" once and render NeuProvider at the root. Server components aren't a concept outside Next/Remix, so you can use any NeumorUI component freely from any file.
Common pitfalls
- Forgot "use client": If you see "You're importing a component that needs useState/useEffect", add
"use client"to the top of the file. - Two providers: Don't nest
NeuProvider. Mount it once at the app root. - Missing stylesheet: If components look unstyled, you forgot to import
"neumorui/styles".