@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 new Context-based theming system.
⚠️ Important: This guide is for MeoNode UI v0.3.0+ which introduced breaking changes to the theme system. If you're upgrading from v0.2.x, please see the Migration Guide.
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 yarn add @meonode/ui @emotion/cache
Configure Emotion's CSS-in-JS functionality. This is optional, as the library will still function without it, but enabling it ensures full support for advanced CSS-in-JS features and React Server Components compatibility.
import type { NextConfig } from 'next' const nextConfig: NextConfig = { // Enables Emotion's CSS-in-JS features (optional) compiler: { emotion: true, }, // Optimizes module imports to improve build performance experimental: { optimizePackageImports: ['@meonode/ui'], }, } export default nextConfig
src/constants/themes/)Define your light and dark themes following the new structure required by MeoNode UI. Each theme must include a mode
and a system object containing your design tokens.
import { Theme } from '@meonode/ui' const lightTheme: Theme = { mode: 'light', system: { primary: { default: '#2196F3', content: '#FFFFFF', }, secondary: { default: '#9C27B0', content: '#FFFFFF', }, accent: { default: '#FF9800', content: '#000000', }, base: { default: '#FFFFFF', content: '#1A1A1A', }, surface: { default: '#F5F5F5', content: '#333333', }, success: { default: '#4CAF50', content: '#FFFFFF', }, warning: { default: '#FF9800', content: '#000000', }, error: { default: '#F44336', content: '#FFFFFF', }, spacing: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px', '2xl': '48px', }, breakpoint: { xs: '480px', sm: '640px', md: '768px', lg: '1024px', xl: '1280px', '2xl': '1536px', }, text: { xs: '0.75rem', sm: '0.875rem', base: '1rem', lg: '1.125rem', xl: '1.25rem', '2xl': '1.5rem', '3xl': '1.875rem', '4xl': '2.25rem', }, }, } export default lightTheme
import { Theme } from '@meonode/ui' const darkTheme: Theme = { mode: 'dark', system: { primary: { default: '#2196F3', content: '#FFFFFF', }, secondary: { default: '#9C27B0', content: '#FFFFFF', }, accent: { default: '#FF9800', content: '#000000', }, base: { default: '#121212', content: '#E0E0E0', }, surface: { default: '#1E1E1E', content: '#FFFFFF', }, success: { default: '#4CAF50', content: '#FFFFFF', }, warning: { default: '#FF9800', content: '#000000', }, error: { default: '#F44336', content: '#FFFFFF', }, spacing: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px', '2xl': '48px', }, breakpoint: { xs: '480px', sm: '640px', md: '768px', lg: '1024px', xl: '1280px', '2xl': '1536px', }, text: { xs: '0.75rem', sm: '0.875rem', base: '1rem', lg: '1.125rem', xl: '1.25rem', '2xl': '1.5rem', '3xl': '1.875rem', '4xl': '2.25rem', }, }, } export default darkTheme
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. It defines a slice for theme management and creates typed hooks for a better developer experience.
// Import necessary Redux Toolkit and React-Redux utilities import { configureStore, Store, UnknownAction } from '@reduxjs/toolkit' import { Provider, useDispatch, useSelector } from 'react-redux' import { createNode } from '@meonode/ui' import { setupListeners } from '@reduxjs/toolkit/query' import appSlice, { AppState, setIsMobile } from '@src/redux/slice/app.slice' export interface RootState { app: AppState } let globalStore: Store<RootState, UnknownAction> | undefined export const initializeStore = (preloadedState?: RootState): Store<RootState, UnknownAction> => { if (!globalStore) { globalStore = configureStore({ reducer: { app: appSlice, }, preloadedState, }) setupListeners(globalStore.dispatch) } else if (preloadedState) { globalStore.dispatch(setIsMobile(preloadedState.app.isMobile)) } return globalStore } export type AppDispatch = ReturnType<typeof initializeStore>['dispatch'] export const useAppDispatch = useDispatch.withTypes<AppDispatch>() export const useAppSelector = useSelector.withTypes<RootState>() export const ReduxProviderWrapper = createNode(Provider)
src/components/Wrapper.ts)This file defines the Wrapper component that wraps the entire app with Redux and Theme providers. It also includes a
PortalWrapper component for modals and overlays, ensuring they have access to the theme context.
'use client' import { Children, Node, Theme, ThemeProvider as MeoThemeProvider } from '@meonode/ui' import { initializeStore, ReduxProviderWrapper, RootState } from '@src/redux/store' import { StrictMode, useEffect, useMemo, useState } 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' const ThemeProvider = ({ children, isPortal, theme }: { children?: Children; isPortal?: boolean; theme?: Theme }) => { const [loadedTheme, setLoadedTheme] = useState<Theme | undefined>(theme) useEffect(() => { if (!theme) { const stored = localStorage.getItem('theme') if (!stored) { const isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches setLoadedTheme(isDark ? darkTheme : lightTheme) } else { setLoadedTheme(stored === 'dark' ? darkTheme : lightTheme) } } }, [theme]) useEffect(() => { if (isPortal) { const handleStorageChange = (e: StorageEvent) => { if (e.key === 'theme') { setLoadedTheme(e.newValue === 'dark' ? darkTheme : lightTheme) } } window.addEventListener('storage', handleStorageChange) return () => { window.removeEventListener('storage', handleStorageChange) } } }, [isPortal]) return MeoThemeProvider({ theme: loadedTheme, children }).render() } export const Wrapper = ({ preloadedState, initialThemeMode, children, isPortal = false, }: { preloadedState?: RootState initialThemeMode?: Theme['mode'] children?: Children isPortal?: boolean }) => { const initialStore = useMemo(() => initializeStore(preloadedState), [preloadedState]) const theme = useMemo(() => { switch (initialThemeMode) { case 'dark': return darkTheme case 'light': default: return lightTheme } }, [initialThemeMode]) return Node(StrictMode, { children: StyleRegistry({ children: [ CssBaseline(), ReduxProviderWrapper({ store: initialStore, children: ThemeProvider({ theme, isPortal, children }), }), ], }), }).render() } export const PortalWrapper = Node(StrictMode, { children: Node(Wrapper, { isPortal: true }), })
src/app/layout.ts)This is the main layout file for Next.js using the App Router. It sets up global styles, fonts, and the
ProvidersWrapper to ensure the entire app has access to Redux and the theme context.
import type { Metadata } from 'next' import { Geist, Geist_Mono } from 'next/font/google' import '@src/app/globals.css' import { Html, Body, Node } from '@meonode/ui' import { cookies, headers } from 'next/headers' import darkTheme from '@src/constants/themes/darkTheme' import lightTheme from '@src/constants/themes/lightTheme' import { ReactNode } from 'react' import { StyleRegistry } from '@meonode/ui/nextjs-registry' 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: 'MeoNode UI with Next.js', description: 'Type-safe React UI without JSX', } 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 initialThemeMode = (cookieStore.get('theme')?.value as 'light' | 'dark') || 'light' const preloadedState: RootState = { app: { isMobile, }, } // Select theme based on initial mode const selectedTheme = initialThemeMode === 'dark' ? darkTheme : lightTheme return Html({ lang: 'en', className: initialThemeMode === 'dark' ? 'dark-theme' : 'light-theme', 'data-theme': initialThemeMode, children: [ Body({ className: `${geistSans.variable} ${geistMono.variable} font-sans`, children: Node(Wrapper, { preloadedState, initialTheme: selectedTheme, children, }), }), ], }).render() }
src/app/page.ts)Page components now benefit from automatic theme resolution through Context without needing to pass theme props to individual components.
'use client' import { Center, Column, H1, Button, Text, Portal, A, Row, Div } from '@meonode/ui' import { PortalWrapper } from '@src/components/Wrapper' import { useEffect, useState } from 'react' export default function HomePage() { return Center({ minHeight: '100dvh', padding: 'theme.spacing.xl', backgroundColor: 'theme.base', color: 'theme.base.content', children: Column({ gap: 'theme.spacing.lg', maxWidth: '800px', textAlign: 'center', children: [ H1('Welcome to MeoNode UI', { fontSize: 'theme.text.3xl', color: 'theme.primary', marginBottom: 'theme.spacing.md', }), Text('Build React UIs with type-safe fluency without JSX syntax', { fontSize: 'theme.text.lg', lineHeight: 1.6, marginBottom: 'theme.spacing.xl', }), // Feature highlights Row({ gap: 'theme.spacing.md', justifyContent: 'center', flexWrap: 'wrap', marginBottom: 'theme.spacing.xl', children: [ FeatureBadge('🚀 No JSX'), FeatureBadge('🎯 Type-Safe'), FeatureBadge('🌍 Context Themes'), FeatureBadge('⚡ RSC Ready'), ], }), // Action buttons Row({ gap: 'theme.spacing.lg', justifyContent: 'center', flexWrap: 'wrap', children: [ A({ backgroundColor: 'theme.primary', color: 'theme.primary.content', padding: 'theme.spacing.md theme.spacing.xl', borderRadius: '12px', fontSize: 'theme.text.lg', fontWeight: 600, textDecoration: 'none', border: '2px solid transparent', transition: 'all 0.3s ease', css: { '&:hover': { transform: 'translateY(-2px)', boxShadow: '0 8px 16px rgba(33, 150, 243, 0.3)', borderColor: 'theme.primary.content', }, }, href: 'https://ui.meonode.com/docs/getting-started/overview', rel: 'noopener noreferrer', children: '📚 Explore Docs', }), Button('🎯 Interactive Demo', { backgroundColor: 'theme.secondary', color: 'theme.secondary.content', padding: 'theme.spacing.md theme.spacing.xl', borderRadius: '12px', fontSize: 'theme.text.lg', fontWeight: 600, cursor: 'pointer', transition: 'all 0.3s ease', css: { '&:hover': { transform: 'translateY(-2px)', boxShadow: '0 8px 16px rgba(156, 39, 176, 0.2)', }, }, onClick: () => InteractiveModal(), }), ], }), ], }), }).render() } // Feature badge component const FeatureBadge = (text: string) => Div({ backgroundColor: 'theme.surface', color: 'theme.surface.content', padding: 'theme.spacing.sm theme.spacing.md', borderRadius: '20px', fontSize: 'theme.text.sm', fontWeight: 500, border: '1px solid theme.primary', children: text, }) // Enhanced Interactive Modal const InteractiveModal = Portal<{}>(PortalWrapper, ({ portal }) => { const [count, setCount] = useState(0) const [selectedColor, setSelectedColor] = useState('primary') useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') portal.unmount() } document.addEventListener('keydown', handleEscape) return () => document.removeEventListener('keydown', handleEscape) }, [portal]) return Center({ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(8px)', zIndex: 1000, onClick: e => { if (e.currentTarget === e.target) portal.unmount() }, children: [ Column({ backgroundColor: 'theme.base', color: 'theme.base.content', padding: 'theme.spacing.xl', borderRadius: '20px', maxWidth: '500px', margin: 'theme.spacing.lg', boxShadow: '0 20px 40px rgba(0,0,0,0.3)', gap: 'theme.spacing.lg', css: { animation: 'slideIn 0.3s ease-out', '@keyframes slideIn': { from: { opacity: 0, transform: 'scale(0.9) translateY(-20px)' }, to: { opacity: 1, transform: 'scale(1) translateY(0)' }, }, }, children: [ H1('🎯 Interactive Demo', { fontSize: 'theme.text.2xl', textAlign: 'center', color: `theme.${selectedColor}`, // Dynamic theme resolution }), Text('Context-based theming in action!', { textAlign: 'center', opacity: 0.8, fontSize: 'theme.text.base', }), // Counter demo Column({ gap: 'theme.spacing.md', padding: 'theme.spacing.lg', backgroundColor: 'theme.surface', borderRadius: '12px', children: [ Text(`Counter: ${count}`, { fontSize: 'theme.text.xl', textAlign: 'center', fontWeight: 600, color: `theme.${selectedColor}`, }), Row({ gap: 'theme.spacing.md', justifyContent: 'center', children: [ Button('-', { backgroundColor: 'theme.error', color: 'theme.error.content', padding: 'theme.spacing.sm theme.spacing.md', borderRadius: '8px', cursor: 'pointer', fontWeight: 600, onClick: () => setCount(c => Math.max(0, c - 1)), }), Button('+', { backgroundColor: 'theme.success', color: 'theme.success.content', padding: 'theme.spacing.sm theme.spacing.md', borderRadius: '8px', cursor: 'pointer', fontWeight: 600, onClick: () => setCount(c => c + 1), }), ], }), ], }), // Theme color selector Column({ gap: 'theme.spacing.sm', children: [ Text('Theme Colors:', { fontSize: 'theme.text.sm', textAlign: 'center', fontWeight: 500, }), Row({ gap: 'theme.spacing.sm', justifyContent: 'center', children: ['primary', 'secondary', 'accent', 'success'].map(color => Button(color, { backgroundColor: selectedColor === color ? `theme.${color}` : 'transparent', color: selectedColor === color ? `theme.${color}.content` : `theme.${color}`, border: `2px solid theme.${color}`, padding: 'theme.spacing.sm theme.spacing.md', borderRadius: '6px', cursor: 'pointer', fontSize: 'theme.text.sm', textTransform: 'capitalize', transition: 'all 0.2s ease', onClick: () => setSelectedColor(color), }), ), }), ], }), Button('Close Modal (ESC)', { backgroundColor: 'theme.base.content', color: 'theme.base', padding: 'theme.spacing.md theme.spacing.lg', borderRadius: '10px', cursor: 'pointer', fontWeight: 500, css: { '&:hover': { opacity: 0.8, }, }, onClick: () => portal.unmount(), }), ], }), ], }) })
theme prop removed in favor of ThemeProvidermode and system properties// ❌ No longer works in v0.3+ const MyComponent = () => Div({ theme: myTheme, backgroundColor: 'theme.primary', children: 'Hello World', })
// ✅ v0.3+ approach const App = () => ThemeProvider({ theme: myTheme, // New theme structure required children: [ Div({ backgroundColor: 'theme.primary', // Automatic resolution children: 'Hello World', }), ], })
Update your theme objects to follow the new v0.3+ structure:
// Before (v0.2.x) const oldTheme = { primary: '#2196F3', secondary: '#9C27B0', // ... other properties } // After (v0.3+) const newTheme: Theme = { mode: 'light', // Required system: { // Required wrapper primary: { default: '#2196F3', content: '#FFFFFF', }, secondary: { default: '#9C27B0', content: '#FFFFFF', }, // ... other properties }, }
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.
Repository Details:
Server Components: MeoNode UI components with RSC support can be rendered on the server, but interactive
components using hooks must be declared with 'use client'.
Theme Provider Placement: Always wrap your app with ThemeProvider at the highest level to ensure theme context
is available throughout your component tree.
State Management: Use Redux Toolkit or Zustand for complex global state. The theme system now uses React Context, eliminating the need for theme-related state management.
Performance: Utilize Next.js's built-in optimizations and MeoNode UI's automatic CSS optimization through Emotion.
Portal Components: Use the updated PortalWrapper wrapper for modals and overlays to ensure they have access to
the theme context.
Theme Switching: Implement theme switching using useState or a theme management library, updating the
ThemeProvider's theme prop.
Accessibility: Ensure your components include proper ARIA attributes and follow semantic HTML principles for a better user experience.
On this page