• 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.

'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 { StyleRegistry } from '@meonode/ui/nextjs-registry'
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])
  const theme = useMemo(() => {
    switch (themeMode) {
      case 'dark':
        return darkTheme
      case 'light':
        return lightTheme
      default:
        if (typeof window === 'undefined') return darkTheme
        const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
        return isDarkMode ? darkTheme : lightTheme
    }
  }, [themeMode])

  return Node(StrictMode, {
    children: StyleRegistry({
      children: ReduxProvider({
        store,
        children: PortalProvider({
          children: [
            CssBaseline(),
            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 and wraps the app in StyleRegistry for SSR compatibility.

import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'
import { Body, Html, Node } from '@meonode/ui'
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: Node(Wrapper, {
        preloadedState,
        themeMode,
        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. Next.js Style Registry: Wrap your application with StyleRegistry to ensure CSS-in-JS styles are collected and injected during SSR.
  3. usePortal Hook: Prefer usePortal for all overlays. It manages the portal stack automatically and provides full type safety.
  4. 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