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

Theming — MeoNode UI

Context-based theming with automatic value resolution. Define your theme object, wrap your app with ThemeProvider, and reference theme tokens anywhere using string paths—no prop drilling required.


Theme Structure

Every theme requires two root properties:

interface Theme {
  mode: 'light' | 'dark' | string
  system: {
    primary: { default: string; content: string }
    secondary: { default: string; content: string }
    accent: { default: string; content: string }
    warning: { default: string; content: string }
    base: { default: string; content: string; accent?: string }
    // Add custom tokens as needed
  }
}

Both mode and system are user-typeable via the MeoTheme interface — see Typed Theme Tokens below.

Required:

  • mode — Theme mode identifier (light, dark, or custom)
  • system — Design tokens (colors, spacing, typography, etc.)

Example Theme:

export const lightTheme: Theme = {
  mode: 'light',
  system: {
    primary: {
      default: '#FF6B6B',
      content: '#4A0000',
    },
    secondary: {
      default: '#6BCB77',
      content: '#0A3B0F',
    },
    accent: {
      default: '#4ECDC4',
      content: '#1A4A47',
    },
    warning: {
      default: '#FFE66D',
      content: '#665A00',
    },
    base: {
      default: '#F8F8F8',
      content: '#333333',
      accent: '#88B04B',
    },
  },
  // Add custom properties for your design system
}

ThemeProvider

Wrap your app with ThemeProvider to enable theme access throughout the component tree:

import { ThemeProvider, Column, H1, Text } from '@meonode/ui'
import { lightTheme } from './theme'

const AppContent = () =>
  Column({
    padding: 20,
    children: [
      H1('Themed Application', {
        color: 'theme.primary',
        fontSize: '2.5rem',
      }),
      Text('Automatic theme resolution', {
        backgroundColor: 'theme.base',
        color: 'theme.base.content',
        padding: 10,
      }),
    ],
  }).render()

const App = () =>
  ThemeProvider({
    theme: lightTheme,
    children: AppContent(),
  }).render()

All MeoNode components inside ThemeProvider automatically access the theme—no prop passing required.


Accessing Theme Values

String Path Notation

Reference theme tokens using dot-separated paths prefixed with theme.:

import { Column, Button } from '@meonode/ui'

Column({
  backgroundColor: 'theme.primary',
  padding: 'theme.base.accent',
  children: Button('Submit', {
    backgroundColor: 'theme.secondary',
    color: 'theme.secondary.content',
  }),
})

Default Key Resolution:
Partial paths (e.g., 'theme.primary') automatically resolve to the default key within that object. If no default exists, it throws an error.

// These are equivalent:
backgroundColor: 'theme.primary'
backgroundColor: 'theme.primary.default'

Direct Object Access

Use the useTheme hook for direct access with full TypeScript support:

import { useTheme, Column, Text } from '@meonode/ui'

const ThemedComponent = () => {
  const { theme } = useTheme()

  return Column({
    padding: theme.system.accent.default,
    backgroundColor: theme.system.base.default,
    children: Text('Direct access', {
      color: theme.system.base.content,
    }),
  }).render()
}

Theme Functions in css Prop

Pass functions to css prop properties for dynamic theme-based computations:

import { Div } from '@meonode/ui'
import tinycolor from 'tinycolor2'

Div({
  backgroundColor: 'theme.primary',
  css: {
    // Simple theme access
    color: theme => theme.system.primary.content,

    // Computed values
    boxShadow: theme => `0 4px 14px 0 ${tinycolor(theme.system.primary.default).setAlpha(0.38).toString()}`,
  },
})

This provides maximum flexibility for complex styling logic based on theme tokens.


Typed Theme Tokens

By default, any 'theme.<anything>' string is accepted on style props — useful, but you get no autocomplete and typos are silent until runtime.

To opt in to autocomplete and compile-time validation of token paths, augment the MeoTheme interface once in your project (e.g. in a meonode.d.ts file at your src root):

declare module '@meonode/ui' {
  interface MeoTheme {
    mode: 'light' | 'dark' | 'sepia'
    system: {
      primary: { default: string; content: string }
      secondary: { default: string; content: string }
      base: { default: string; content: string }
      spacing: { sm: number; md: number; lg: number }
    }
  }
}

Once declared, token strings narrow to a literal union and theme.mode / theme.system reflect your shape:

import { Div, useTheme } from '@meonode/ui'

// ✅ Autocompletes and type-checks every path
Div({
  color: 'theme.primary',           // suggested
  backgroundColor: 'theme.base',    // suggested
  padding: 'theme.spacing.md',      // suggested
})

// ❌ Compile error — no such token
Div({ color: 'theme.primry' })

// ✅ Typed mode + system in function form
Div({
  css: {
    color: theme =>
      theme.mode === 'sepia'
        ? theme.system.primary.default
        : theme.system.secondary.default,
  },
})

Notes:

  • Both mode and system keys are independently optional — augment whichever you want typed.
  • Without augmentation, behavior is unchanged: any 'theme.…' string is allowed.
  • Plain CSS values ('red', 'flex', 16) keep working alongside token strings — autocomplete for native CSS keywords is preserved.
  • The useTheme hook returns the same augmented Theme type, so theme.system.<path> autocompletes too.

Function Children

Function children automatically inherit theme context:

import { ThemeProvider, Column } from '@meonode/ui'

ThemeProvider({
  theme: myTheme,
  children: () =>
    Column({
      color: 'theme.accent',
      padding: 'theme.spacing.md',
    }),
})

The child function's returned nodes receive the theme automatically.


Complete Example

import { ThemeProvider, Column, H1, Button, Text } from '@meonode/ui'

const lightTheme = {
  mode: 'light',
  system: {
    primary: { default: '#3B82F6', content: '#FFFFFF' },
    secondary: { default: '#10B981', content: '#FFFFFF' },
    base: { default: '#F9FAFB', content: '#1F2937' },
    spacing: { sm: 8, md: 16, lg: 24 },
  },
}

const ThemedApp = () =>
  Column({
    padding: 'theme.spacing.lg',
    backgroundColor: 'theme.base',
    children: [
      H1('MeoNode Theming', {
        color: 'theme.primary',
        marginBottom: 'theme.spacing.md',
      }),
      Text('Context-based theme system with automatic resolution', {
        color: 'theme.base.content',
        marginBottom: 'theme.spacing.md',
      }),
      Button('Primary Action', {
        backgroundColor: 'theme.primary',
        color: 'theme.primary.content',
        padding: '12px 24px',
        borderRadius: 8,
      }),
    ],
  }).render()

const App = () =>
  ThemeProvider({
    theme: lightTheme,
    children: ThemedApp(),
  }).render()

export default App

Multiple Themes

Switch themes dynamically or nest ThemeProvider for different theme contexts:

import { useState } from 'react'
import { ThemeProvider, Column, Button } from '@meonode/ui'

const darkTheme = {
  mode: 'dark',
  system: {
    primary: { default: '#60A5FA', content: '#1E3A8A' },
    base: { default: '#1F2937', content: '#F9FAFB' },
  },
}

const App = () => {
  const [theme, setTheme] = useState(lightTheme)

  return ThemeProvider({
    theme,
    children: Column({
      children: [
        Button('Toggle Theme', {
          onClick: () => setTheme(theme.mode === 'light' ? darkTheme : lightTheme),
        }),
      ],
    }),
  }).render()
}

Best Practices

Structure system Property
Organize design tokens logically: colors, spacing, typography, shadows, borders.

Use Semantic Names
primary, secondary, success, warning over blue, green.

Default Keys for Variants
Use default as the base value, add content, hover, active for variants.

system: {
  primary: {
    default: '#3B82F6',
    content: '#FFFFFF',
    hover: '#2563EB',
    active: '#1D4ED8'
  }
}

TypeScript for Type Safety
Augment the MeoTheme interface to get autocomplete and compile-time validation on 'theme.<path>' strings — see Typed Theme Tokens.

Function-Based Computations
Use theme functions in css prop for dynamic color transformations or complex calculations.


Common Patterns

Color System

system: {
  // Core colors
  primary: { default: '#3B82F6', content: '#FFFFFF' },
  secondary: { default: '#10B981', content: '#FFFFFF' },

  // Semantic colors
  success: { default: '#10B981', content: '#FFFFFF' },
  warning: { default: '#F59E0B', content: '#78350F' },
  error: { default: '#EF4444', content: '#FFFFFF' },

  // Surface colors
  base: { default: '#FFFFFF', content: '#1F2937' },
  surface: { default: '#F9FAFB', content: '#374151' },
  overlay: { default: 'rgba(0, 0, 0, 0.5)', content: '#FFFFFF' }
}

Spacing Scale

system: {
  spacing: {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
    xl: 32,
    '2xl': 48
  }
}

Typography

system: {
  typography: {
    fontFamily: {
      sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto',
      mono: 'ui-monospace, SFMono-Regular, Menlo'
    },
    fontSize: {
      sm: '0.875rem',
      base: '1rem',
      lg: '1.125rem',
      xl: '1.25rem',
      '2xl': '1.5rem'
    },
    fontWeight: {
      normal: 400,
      medium: 500,
      semibold: 600,
      bold: 700
    }
  }
}

How does the theming system work?

Wrap your app with ThemeProvider, define tokens under theme.system, and reference values using token paths like theme.primary or through useTheme().

How does styling work in MeoNode UI?

Styling is prop-driven and theme-aware. Use direct CSS props for simple styles, and use the css prop for selectors, queries, and keyframes.

More details: /docs/getting-started/faq


Next Steps

  • Framework Integration — Next.js, Vite, Remix configuration
  • FAQ — Common patterns, troubleshooting, migration strategies
  • Release Notes — Changelog, breaking changes, upgrade guides

On this page