• Getting Started
    • Overview
    • Why Without JSX?
    • Installation
    • Usage
    • Styling
    • Theming
    • Portal System
    • Rules & Patterns
    • Framework Integration
    • FAQ
    • Release Notes
  • MUI Integration
  • Components
  • Hooks

Next.js Integration Guide for MeoNode UI

Overview

@meonode/ui is a modern, type-safe React UI library designed for seamless integration with popular frameworks, especially Next.js. This guide provides a comprehensive, step-by-step walkthrough for setting up a Next.js project with MeoNode UI, demonstrating real-world integration patterns with the Context-based theming system and the Portal system.


Next.js Next.js Integration

Installation & Setup

Begin your project by using the recommended Next.js CLI to set up a new application. This ensures your project is pre-configured with the latest best practices.

# Create a new Next.js app with TypeScript support
npx create-next-app@latest my-app --typescript

# Navigate to your project directory
cd my-app

# Install the core MeoNode UI library and peer dependencies
yarn add @meonode/ui @emotion/cache @emotion/react @emotion/styled

Next.js Configuration

Configure Emotion's CSS-in-JS functionality and optimize imports.

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // Enables Emotion's CSS-in-JS features
  compiler: {
    emotion: true,
  },
  // Optimizes module imports to improve build performance
  experimental: {
    optimizePackageImports: ['@meonode/ui'],
  },
}

export default nextConfig

Theme Configuration (src/constants/themes/)

Define your design tokens in a central system and create light/dark theme variants. Each theme must include a mode and a system object containing your tokens.

const themeSystem = {
  text: {
    xs: '0.75rem',
    sm: '0.875rem',
    md: '1rem',
    lg: '1.125rem',
    xl: '1.25rem',
    '2xl': '1.5rem',
    '3xl': '1.875rem',
  },
  spacing: {
    xs: '0.25rem',
    sm: '0.5rem',
    md: '1rem',
    lg: '1.5rem',
    xl: '2rem',
    '2xl': '3rem',
  },
  radius: {
    sm: '2px',
    md: '4px',
    lg: '8px',
    xl: '16px',
    full: '9999px',
  },
}
export default themeSystem
import { Theme } from '@meonode/ui'
import themeSystem from './themeSystem'

const lightTheme: Theme = {
  mode: 'light',
  system: {
    ...themeSystem,
    primary: { default: '#2196F3', content: '#FFFFFF' },
    secondary: { default: '#9C27B0', content: '#FFFFFF' },
    base: { default: '#FFFFFF', content: '#1A1A1A' },
  },
}
export default lightTheme

Store (src/redux/store.ts)

This file sets up a singleton Redux store, making it accessible from both the server and client in a Next.js environment.

import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import appSlice from '@src/redux/slice/app.slice'
import { createNode } from '@meonode/ui'
import { Provider } from 'react-redux'

export const initializeStore = (preloadedState?: object) => {
  const store = configureStore({
    reducer: {
      app: appSlice.reducer,
    },
    middleware: getDefaultMiddleware =>
      getDefaultMiddleware()
        .concat
        // your middleware
        (),
    preloadedState,
  })

  setupListeners(store.dispatch)
  return store
}

export type RootState = ReturnType<ReturnType<typeof initializeStore>['getState']>
export type AppDispatch = ReturnType<typeof initializeStore>['dispatch']
export const ReduxProvider = createNode(Provider)

Wrapper Components (src/components/Wrapper.ts)

The Wrapper component is the heart of your client-side providers, integrating Redux, the Theme system, and the Portal System. Keep StyleRegistry out of Wrapper — mount it once in the root layout instead (see below).

'use client'
import { Children, Node, PortalHost, PortalProvider, Theme, ThemeProvider } from '@meonode/ui'
import { StrictMode, useMemo } from 'react'
import { CssBaseline } from '@meonode/mui'
import darkTheme from '@src/constants/themes/darkTheme'
import lightTheme from '@src/constants/themes/lightTheme'
import { initializeStore, ReduxProvider, RootState } from '@src/redux/store'

export const Wrapper = ({
  preloadedState,
  themeMode,
  children,
}: {
  preloadedState?: Partial<RootState>
  themeMode?: Theme['mode']
  children?: Children
  isPortal?: boolean
}) => {
  const store = useMemo(() => initializeStore(preloadedState), [preloadedState])
  // Resolve theme from the server-passed cookie value so SSR and hydration agree.
  const theme = useMemo(() => (themeMode === 'dark' ? darkTheme : lightTheme), [themeMode])

  return Node(StrictMode, {
    children: PortalProvider({
      children: [
        CssBaseline(),
        ReduxProvider({
          store,
          children: ThemeProvider({
            theme,
            children: Array.isArray(children) ? children.concat(PortalHost()) : [children, PortalHost()],
          }),
        }),
      ],
    }),
  }).render()
}

App Router Integration (src/app/layout.ts)

This is the main layout file for Next.js. It uses userAgent to detect devices for preloaded state, reads the theme cookie on the server, and wraps the entire app in StyleRegistry at the root layout so every route segment shares one Emotion cache during SSR.

import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'
import { Body, Html, Node } from '@meonode/ui'
import { StyleRegistry } from '@meonode/ui/nextjs-registry'
import { cookies, headers } from 'next/headers'
import { ReactNode } from 'react'
import { RootState } from '@src/redux/store'
import { Wrapper } from '@src/components/Wrapper'
import { userAgent } from 'next/server'

const geistSans = Geist({
  variable: '--font-geist-sans',
  subsets: ['latin'],
})

const geistMono = Geist_Mono({
  variable: '--font-geist-mono',
  subsets: ['latin'],
})

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default async function RootLayout({ children }: { children: ReactNode }) {
  const reqHeaders = await headers()
  const ua = userAgent({ headers: reqHeaders })
  const isMobile = ua.device.type === 'mobile' || ua.device.type === 'tablet'

  const cookieStore = await cookies()
  const themeMode = cookieStore.get('theme')?.value as 'light' | 'dark'

  const preloadedState: RootState = {
    app: {
      isMobile,
    },
  }

  return Html({
    lang: 'en',
    className: themeMode === 'dark' ? 'dark-theme' : 'light-theme',
    'data-theme': themeMode,
    children: Body({
      className: `${geistSans.variable} ${geistMono.variable} font-sans`,
      children: StyleRegistry({
        children: Node(Wrapper, {
          preloadedState,
          themeMode,
          children,
        }),
      }),
    }),
  }).render()
}

Nested route layouts (docs, dashboards, etc.)

Keep metadata and data fetching in server layout.ts files, but move styled MeoNode chrome (sidebars, navbars, page frames with Root / Column / Row) into 'use client' shell components. Render them from the server layout with Node(MyShell, { children }).render().

Without that client boundary, nested server layouts can SSR styled nodes outside the root StyleRegistry cache and cause hydration mismatches or broken layout — even when StyleRegistry is correctly placed at the root.

'use client'
import { Column, Main, Node, Root, Row } from '@meonode/ui'
import type { ReactNode } from 'react'

export default function DocsSectionShell({ children }: { children?: ReactNode }) {
  return Root({
    backgroundColor: 'theme.base',
    color: 'theme.base.content',
    children: [
      /* Navbar, Sidebar, etc. */
      Main({ children }),
    ],
  }).render()
}
import { Node } from '@meonode/ui'
import DocsSectionShell from '@src/components/DocsSectionShell'

export default function DocsLayout({ children }: { children: React.ReactNode }) {
  return Node(DocsSectionShell, { children }).render()
}

Page Components & Portals (src/app/page.ts)

Use the usePortal hook for managing overlays. Modals are standard components that receive close and data props.

'use client'
import { Center, Column, H1, Button, Text, usePortal } from '@meonode/ui'

export default function HomePage() {
  const portal = usePortal()

  return Center({
    children: Column({
      children: [
        H1('Welcome to MeoNode UI'),
        Button('Open Modal', {
          onClick: () => portal.open(MyModal, { name: 'Developer' })
        })
      ]
    })
  }).render()
}

// Modal Component
const MyModal = ({ data, close }: { data: { name: string }, close: () => void }) => 
  Center({
    position: 'fixed',
    inset: 0,
    backgroundColor: 'rgba(0,0,0,0.5)',
    backdropFilter: 'blur(4px)',
    children: Column({
      backgroundColor: 'theme.base',
      padding: 'theme.spacing.xl',
      borderRadius: 'theme.radius.lg',
      children: [
        Text(`Hello ${data.name}!`),
        Button('Close', { onClick: close })
      ]
    })
  }).render()

Boilerplate / Example Repository

Check out the nextjs-meonode repository for a complete, working example of a Next.js project integrated with MeoNode UI. This repository demonstrates best practices, including Context-based theming, Redux Toolkit state management, and React Server Components support.


Best Practices

  1. Portal System: Always include PortalHost() within your ThemeProvider children to ensure portals inherit the theme context.
  2. StyleRegistry at root layout: Import from @meonode/ui/nextjs-registry and wrap your app once in src/app/layout.ts (inside Body, around Wrapper and all route segments). Do not mount a second registry in nested layouts or inside Wrapper.
  3. Styled nested layouts: Use 'use client' shell components for route layouts that render styled MeoNode structure; keep the layout file itself as a server component for metadata.
  4. Theme cookie on server: Pass themeMode from cookies() into Wrapper so SSR and client hydration pick the same light/dark theme. Avoid prefers-color-scheme fallbacks that differ between server and browser.
  5. usePortal Hook: Prefer usePortal for all overlays. It manages the portal stack automatically and provides full type safety.
  6. Semantic Tokens: Always use tokens like theme.primary instead of hardcoded colors for automatic light/dark mode support.

Additional Resources


Why am I getting TypeScript build errors with Page components in Next.js?

Most issues come from page export shape mismatches or components returning non-React elements where Next.js expects valid page output. Keep page modules simple and ensure wrapped components return valid rendered output.

How do I use existing JSX components or JSX libraries with MeoNode UI?

Use Node() to wrap third-party JSX components, then pass their props as normal. This keeps interoperability smooth without rewriting external libraries.

More details: /docs/getting-started/faq

On this page