@meonode/ui is a modern, type-safe React UI library that works perfectly with Vite + React + TypeScript projects. This guide provides a comprehensive setup for integrating MeoNode UI into a Vite-based React application, featuring 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 migrating from v0.2.x, please refer to the Migration Guide.
Create a new Vite project with React and TypeScript support:
# Create a new Vite app with React and TypeScript npm create vite@latest my-app -- --template react-ts # Navigate to your project directory cd my-app # Install the core MeoNode UI library npm install @meonode/ui # Install additional dependencies npm install @reduxjs/toolkit react-redux
Update your Vite configuration to support Emotion (MeoNode UI's CSS engine):
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' import { visualizer } from 'rollup-plugin-visualizer' import * as path from 'node:path' import { dependencies } from './package.json' import { imagetools } from 'vite-imagetools' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import obfuscator from 'vite-plugin-bundle-obfuscator' function renderChunks(deps: Record<string, string>) { const chunks: Record<string, string[]> = {} Object.keys(deps).forEach(key => { if (['react', 'react-dom'].includes(key)) return chunks[key] = [key] }) return chunks } // https://vite.dev/config/ export default defineConfig({ mode: process.env.NODE_ENV, plugins: [ react(), imagetools(), ViteImageOptimizer(), visualizer({ open: true, sourcemap: true, filename: 'bundle_report.html' }), ], build: { minify: 'esbuild', cssMinify: 'esbuild', sourcemap: true, rollupOptions: { output: { manualChunks: { vendor: ['react', 'react-dom'], ...renderChunks(dependencies), }, }, }, }, resolve: { alias: [{ find: '@src', replacement: path.resolve(__dirname, 'src') }], }, })
Create theme objects structure with mode and system properties:
import { Theme } from '@meonode/ui' const lightTheme: Theme = { mode: 'light', system: { primary: { default: '#3B82F6', content: '#FFFFFF', }, secondary: { default: '#6B7280', content: '#FFFFFF', }, accent: { default: '#F59E0B', content: '#000000', }, base: { default: '#FFFFFF', content: '#111827', }, surface: { default: '#F9FAFB', content: '#374151', }, success: { default: '#10B981', content: '#FFFFFF', }, warning: { default: '#F59E0B', content: '#000000', }, error: { default: '#EF4444', content: '#FFFFFF', }, spacing: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px', '2xl': '48px', }, 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: '#2563EB', content: '#FFFFFF', }, secondary: { default: '#9CA3AF', content: '#000000', }, accent: { default: '#D97706', content: '#FFFFFF', }, base: { default: '#111827', content: '#F3F4F6', }, surface: { default: '#1F2937', content: '#E5E7EB', }, success: { default: '#059669', content: '#FFFFFF', }, warning: { default: '#D97706', content: '#FFFFFF', }, error: { default: '#DC2626', content: '#FFFFFF', }, spacing: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px', '2xl': '48px', }, 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
Since MeoNode UI v0.3+ provides built-in theme management through Context, you only need Redux for your application's business logic, not for theme management.
Set up your Redux store for application state (excluding theme):
import { configureStore } from '@reduxjs/toolkit' import { Provider, useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux' import { Node } from '@meonode/ui' // Import your app-specific slices here // import userSlice from '@src/redux/slice/user.slice' // import appSlice from '@src/redux/slice/app.slice' export interface RootState { // Define your app-specific state here // user: UserState // app: AppState } export const store = configureStore({ reducer: { // Add your app-specific reducers here // user: userSlice, // app: appSlice, }, }) export type AppDispatch = typeof store.dispatch export const useAppDispatch: () => AppDispatch = useDispatch export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
If you need application state management, here's an example slice:
import { createSlice, type PayloadAction } from '@reduxjs/toolkit' export interface AppState { isLoading: boolean user: { name: string email: string } | null } const initialState: AppState = { isLoading: false, user: null, } const appSlice = createSlice({ name: 'app', initialState, reducers: { setLoading: (state, action: PayloadAction<boolean>) => { state.isLoading = action.payload }, setUser: (state, action: PayloadAction<AppState['user']>) => { state.user = action.payload }, clearUser: state => { state.user = null }, }, }) export const { setLoading, setUser, clearUser } = appSlice.actions export default appSlice.reducer
Create a provider component using MeoNode UI's built-in theme management:
import { StrictMode, useEffect, useMemo, useState } from 'react' import { store } from '@src/redux/store' import { type Children, Node, type NodeElement, type Theme } from '@meonode/ui' import { Provider as ReduxProvider } from 'react-redux' import { SnackbarProvider } from 'notistack' import lightTheme from '@src/constants/themes/lightTheme.ts' import darkTheme from '@src/constants/themes/darkTheme.ts' import { ThemeProvider as MeoThemeWrapper } from '@meonode/ui' interface WrappersProps { children: NodeElement } const ThemeWrapper = ({ children }: { children?: Children }) => { const initialTheme = useMemo<Theme>(() => { // Initialize from localStorage const stored = localStorage.getItem('theme') return stored === 'dark' ? darkTheme : lightTheme }, []) return MeoThemeWrapper({ theme: initialTheme, children }).render() } export const Wrapper = ({ children }: WrappersProps) => Node(ReduxProvider, { store, children: Node(ThemeWrapper, { children: Node(SnackbarProvider, { children }) }) }) const PortalThemeWrapper = ({ children }: { children?: Children }) => { const [theme, setTheme] = useState<Theme>(() => { // Initialize from localStorage const stored = localStorage.getItem('theme') return stored === 'dark' ? darkTheme : lightTheme }) useEffect(() => { const handleStorageChange = (e: StorageEvent) => { if (e.key === 'theme') { setTheme(e.newValue === 'dark' ? darkTheme : lightTheme) } } // Listen for changes from other tabs/windows window.addEventListener('storage', handleStorageChange) return () => { window.removeEventListener('storage', handleStorageChange) } }, []) return MeoThemeWrapper({ theme, children }).render() } export const PortalWrapper = Node(StrictMode, { children: Node(ReduxProvider, { store, children: Node(PortalThemeWrapper) }) })
import { createBrowserRouter, type RouteObject } from 'react-router-dom' import { lazy, Suspense } from 'react' import { Center, Node, Text } from '@meonode/ui' import { RouterProvider } from 'react-router-dom' type RouteType = Omit<RouteObject, 'children' | 'element'> & { element?: () => JSX.Element children?: RouteType[] } const App = lazy(() => import('@src/pages/App')) const NotFound = lazy(() => import('@src/pages/NotFound')) const routes: RouteType[] = [ { path: '/', element: App, }, { path: '*', element: NotFound, }, ] const LoadingFallback = Center({ height: '100dvh', backgroundColor: 'theme.base', color: 'theme.base.content', children: Text('Loading...', { fontSize: 'theme.text.lg', }), }).render() const wrapElement = (routes: RouteType[]): RouteObject[] => { return routes.map(route => ({ ...route, element: route.element ? Node(Suspense, { fallback: LoadingFallback, children: Node(route.element), }).render() : undefined, children: route.children ? wrapElement(route.children) : undefined, })) as RouteObject[] } const router = createBrowserRouter(wrapElement(routes)) export default Node(RouterProvider, { router })
Create your main App component using MeoNode UI's built-in theme management:
import { Component, Center, Column, Row, H1, H2, Button, Text, Portal, Div } from '@meonode/ui' import { useAppTheme } from '@src/hooks/useAppTheme' import { PortalWrapper } from '@src/components/Wrapper' import { useState } from 'react' const App = () => { const { theme, mode, toggleTheme, switchToLight, switchToDark } = useAppTheme() const [showDemo, setShowDemo] = useState(false) return Center({ minHeight: '100dvh', padding: 'theme.spacing.xl', backgroundColor: 'theme.base', color: 'theme.base.content', children: Column({ gap: 'theme.spacing.lg', maxWidth: '900px', textAlign: 'center', children: [ H1('Welcome to MeoNode UI', { fontSize: 'theme.text.4xl', color: 'theme.primary', marginBottom: 'theme.spacing.md', fontWeight: 700, }), Text('Build React UIs with type-safe fluency using Vite + TypeScript', { fontSize: 'theme.text.lg', lineHeight: 1.6, marginBottom: 'theme.spacing.xl', opacity: 0.9, }), // Feature highlights Row({ gap: 'theme.spacing.md', justifyContent: 'center', flexWrap: 'wrap', marginBottom: 'theme.spacing.xl', children: [ FeatureBadge('🚀 No JSX'), FeatureBadge('⚡ Vite Powered'), FeatureBadge('🎯 TypeScript'), FeatureBadge('🌍 Built-in Themes'), ], }), // Action buttons Column({ gap: 'theme.spacing.md', children: [ Row({ gap: 'theme.spacing.lg', justifyContent: 'center', flexWrap: 'wrap', children: [ Button('🎯 Interactive Demo', { backgroundColor: 'theme.primary', color: 'theme.primary.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(59, 130, 246, 0.3)', }, }, onClick: () => setShowDemo(true), }), Button(`Toggle Theme (${mode === 'light' ? 'Dark' : 'Light'})`, { 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(107, 114, 128, 0.3)', }, }, onClick: toggleTheme, }), ], }), // Direct theme switcher buttons Row({ gap: 'theme.spacing.sm', justifyContent: 'center', children: [ Button('☀️ Light', { backgroundColor: mode === 'light' ? 'theme.accent' : 'transparent', color: mode === 'light' ? 'theme.accent.content' : 'theme.accent', border: '2px solid theme.accent', padding: 'theme.spacing.sm theme.spacing.md', borderRadius: '8px', fontSize: 'theme.text.sm', cursor: 'pointer', transition: 'all 0.2s ease', onClick: switchToLight, }), Button('🌙 Dark', { backgroundColor: mode === 'dark' ? 'theme.accent' : 'transparent', color: mode === 'dark' ? 'theme.accent.content' : 'theme.accent', border: '2px solid theme.accent', padding: 'theme.spacing.sm theme.spacing.md', borderRadius: '8px', fontSize: 'theme.text.sm', cursor: 'pointer', transition: 'all 0.2s ease', onClick: switchToDark, }), ], }), ], }), // Theme info display ThemeInfo(), // Demo modal trigger showDemo ? InteractiveDemo(() => setShowDemo(false)) : null, ], }), }).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', css: { '&:hover': { backgroundColor: 'theme.primary', color: 'theme.primary.content', }, }, children: text, }) // Theme information display const ThemeInfo = Component(() => { const { theme, mode } = useTheme() return Column({ gap: 'theme.spacing.sm', padding: 'theme.spacing.lg', backgroundColor: 'theme.surface', borderRadius: '12px', marginTop: 'theme.spacing.xl', children: [ H2('Current Theme Info', { fontSize: 'theme.text.xl', color: 'theme.primary', marginBottom: 'theme.spacing.sm', }), Text(`Mode: ${mode}`, { fontSize: 'theme.text.base', }), Text(`Primary Color: ${theme.system.primary.default}`, { fontSize: 'theme.text.base', }), Text(`Background: ${theme.system.base.default}`, { fontSize: 'theme.text.base', }), // Color palette preview Row({ gap: 'theme.spacing.sm', justifyContent: 'center', marginTop: 'theme.spacing.md', children: [ ColorSwatch('primary'), ColorSwatch('secondary'), ColorSwatch('accent'), ColorSwatch('success'), ColorSwatch('warning'), ColorSwatch('error'), ], }), ], }) }) // Color swatch component const ColorSwatch = (colorKey: string) => Div({ width: '24px', height: '24px', backgroundColor: `theme.${colorKey}`, borderRadius: '50%', border: '2px solid theme.base.content', title: colorKey, }) // Interactive demo modal const InteractiveDemo = (onClose: () => void) => Portal<{}>(PortalWrapper, ({ portal }) => { const [counter, setCounter] = useState(0) const { toggleTheme, mode } = useTheme() 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() onClose() } }, children: [ Column({ backgroundColor: 'theme.base', color: 'theme.base.content', padding: 'theme.spacing.2xl', 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('🎯 Vite + MeoNode UI', { fontSize: 'theme.text.2xl', textAlign: 'center', color: 'theme.primary', }), Text('Experience Context-based theming!', { textAlign: 'center', opacity: 0.8, }), // Counter section Column({ gap: 'theme.spacing.md', padding: 'theme.spacing.lg', backgroundColor: 'theme.surface', borderRadius: '12px', children: [ Text(`Counter: ${counter}`, { fontSize: 'theme.text.xl', textAlign: 'center', fontWeight: 600, color: 'theme.primary', }), 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: () => setCounter(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: () => setCounter(c => c + 1), }), ], }), ], }), // Theme toggle in modal Button(`Toggle to ${mode === 'light' ? 'Dark' : 'Light'}`, { backgroundColor: 'theme.accent', color: 'theme.accent.content', padding: 'theme.spacing.md theme.spacing.lg', borderRadius: '10px', cursor: 'pointer', fontWeight: 500, onClick: toggleTheme, }), Button('Close Demo', { 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() onClose() }, }), ], }), ], }) }) export default App
import { Component, Center, Column, H1, Text, Button } from '@meonode/ui' import { useNavigate } from 'react-router-dom' const NotFound = Component(() => { const navigate = useNavigate() return Center({ minHeight: '100dvh', backgroundColor: 'theme.base', color: 'theme.base.content', children: Column({ gap: 'theme.spacing.lg', textAlign: 'center', children: [ H1('404 - Page Not Found', { fontSize: 'theme.text.3xl', color: 'theme.error', }), Text('The page you are looking for does not exist.', { fontSize: 'theme.text.lg', opacity: 0.8, }), Button('Go Home', { backgroundColor: 'theme.primary', color: 'theme.primary.content', padding: 'theme.spacing.md theme.spacing.xl', borderRadius: '8px', cursor: 'pointer', onClick: () => navigate('/'), }), ], }), }) }) export default NotFound
Update your main.ts to include the providers:
import { StrictMode } from 'react' import { Node } from '@meonode/ui' import Routes from '@src/routes' import { Wrapper } from '@src/components/Wrapper.ts' import '@src/assets/global.css' import { render } from '@meonode/ui/client' const App = Node(StrictMode, { children: Wrapper({ children: Routes() }) }) render(App, document.getElementById('root')!)
ThemeProvidermode and system properties// ❌ No longer works in v0.3+ const MyApp = () => Div({ theme: myTheme, backgroundColor: 'theme.colors.primary', children: 'Hello World', })
// ✅ v0.3+ approach const MyApp = () => ThemeProvider({ theme: myTheme, // Must follow new theme structure children: [ Div({ backgroundColor: 'theme.primary', // Automatic resolution children: 'Hello World', }), ], })
Start your development server:
npm run dev
Build for production:
npm run build
Preview the production build:
npm run preview
For a complete boilerplate, explore the meonode-vite repository.
Repository Details:
Context-Based Theming: Always wrap your app with ThemeProvider at the root level to ensure theme context is
available throughout your component tree.
Tree Shaking: Vite's ES modules approach naturally supports tree shaking, so only the MeoNode UI components you use will be included in your bundle.
Hot Module Replacement: Vite provides excellent HMR support. Changes to your MeoNode UI components and theme switching will reflect instantly in the browser.
Code Splitting: Use Vite's dynamic import() to code-split your application at logical points, especially for heavy components or pages.
Theme Persistence: Use localStorage or sessionStorage to persist theme preferences across sessions.
Performance Monitoring: Use Vite's built-in bundle analyzer to monitor your bundle size and optimize imports. The new Context system reduces bundle size by eliminating theme prop drilling.
Portal Components: Use the PortalWrapper wrapper for modals and overlays to ensure they have access to the
theme context outside the main app tree.
Development Experience: Take advantage of Vite's fast refresh and MeoNode UI's Context-based theming for a smooth development experience with instant theme switching.
Build Optimization: Configure Vite's build optimization settings to take full advantage of MeoNode UI's tree-shakable architecture and Emotion's CSS optimization.
Create reusable components that adapt to the current theme using MeoNode's built-in theming:
import { Component, Column, H3, Text, Div, Row, useTheme } from '@meonode/ui' interface ThemeCardProps { title: string description: string variant?: 'primary' | 'secondary' | 'accent' } export const ThemeCard = Component<ThemeCardProps>(({ title, description, variant = 'primary' }) => { const { theme } = useTheme() // MeoNode's built-in hook return Div({ padding: 'theme.spacing.lg', borderRadius: '16px', backgroundColor: 'theme.surface', border: `2px solid theme.${variant}`, css: { '&:hover': { transform: 'translateY(-4px)', boxShadow: `0 8px 32px rgba(${theme.system[variant].default.replace('#', '')}, 0.15)`, }, }, transition: 'all 0.3s ease', children: Column({ gap: 'theme.spacing.md', children: [ H3(title, { color: `theme.${variant}`, fontSize: 'theme.text.xl', fontWeight: 600, }), Text(description, { color: 'theme.surface.content', fontSize: 'theme.text.base', lineHeight: 1.6, }), Row({ justifyContent: 'flex-end', children: [ Div({ width: '12px', height: '12px', borderRadius: '50%', backgroundColor: `theme.${variant}`, }), ], }), ], }), }) })
Create environment-specific configurations:
export const config = { isDev: import.meta.env.DEV, isProd: import.meta.env.PROD, baseUrl: import.meta.env.BASE_URL, // Add other environment variables as needed }
Optimize your build for production:
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [ react({ babel: { plugins: ['@emotion/babel-plugin'], }, }), ], optimizeDeps: { include: ['@meonode/ui'], }, resolve: { alias: { '@src': new URL('./src', import.meta.url).pathname, }, }, build: { rollupOptions: { output: { manualChunks: { vendor: ['react', 'react-dom'], meonode: ['@meonode/ui'], redux: ['@reduxjs/toolkit', 'react-redux'], }, }, }, }, })
On this page