NeumorUINeumorUI
📖📦

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

tsx
// 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

tsx
// 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.

tsx
// 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>
  );
}
tsx
// 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 (no Math.random())
  • All window/document/localStorage access happens inside useEffect or event handlers
  • localStorage theme restore runs after mount (initial render matches server)
  • No useLayoutEffect warnings on the server
  • TypeScript strict mode 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

tsx
// 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

tsx
<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".