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 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
- Portal System: Always include
PortalHost()within yourThemeProviderchildren to ensure portals inherit the theme context. - Next.js Style Registry: Wrap your application with
StyleRegistryto ensure CSS-in-JS styles are collected and injected during SSR. - usePortal Hook: Prefer
usePortalfor all overlays. It manages the portal stack automatically and provides full type safety. - Semantic Tokens: Always use tokens like
theme.primaryinstead of hardcoded colors for automatic light/dark mode support.
Additional Resources
- Live Example: Check out the nextjs-meonode repository to see MeoNode UI in action.
- Core Library: Refer to the official @meonode/ui github for the latest features.
- Release Notes: Check Release Notes for detailed changelog.
Related FAQ
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
- Next.js Integration Guide for MeoNode UI