MeoNode UI
  • Getting Started
    • Overview
    • Installation
    • Usage
    • Styling
    • Theming
    • Rules & Patterns
    • Framework Integration
    • FAQ
    • Release Notes
  • Components

Styling Guide — MeoNode UI

Type-safe, theme-aware CSS-in-JS powered by @emotion/react. Apply styles directly as props or use the css prop for advanced patterns like pseudo-classes, media queries, and animations.


Styling Fundamentals

Direct Prop Styling

Apply CSS properties directly to components:

import { Button } from '@meonode/ui'

Button('Click Me', {
  backgroundColor: 'tomato',
  padding: '12px 24px',
  borderRadius: 8,
  color: 'white',
  cursor: 'pointer',
})

The css Prop

Use the css prop for complex styling patterns:

import { Div } from '@meonode/ui'

Div({
  padding: '20px',
  css: {
    '&:hover': {
      transform: 'scale(1.05)',
    },
    '@media (max-width: 768px)': {
      padding: '12px',
    },
  },
})

Powered by @emotion/react — automatic optimization, vendor prefixing, critical CSS extraction for SSR.


Pseudo-Classes

Interactive States

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

const InteractiveButton = Button('Hover Me', {
  padding: '14px 28px',
  backgroundColor: '#3B82F6',
  color: 'white',
  borderRadius: '10px',
  cursor: 'pointer',
  transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',

  css: {
    '&:hover': {
      backgroundColor: '#2563EB',
      transform: 'translateY(-3px)',
      boxShadow: '0 10px 20px rgba(59, 130, 246, 0.4)',
    },
    '&:active': {
      transform: 'translateY(-1px)',
      transition: 'all 0.1s ease',
    },
    '&:focus': {
      outline: 'none',
      boxShadow: '0 0 0 4px rgba(59, 130, 246, 0.25)',
    },
    '&:focus-visible': {
      outline: '2px solid #1D4ED8',
      outlineOffset: '2px',
    },
    '&:disabled': {
      backgroundColor: '#9CA3AF',
      cursor: 'not-allowed',
      transform: 'none',
    },
  },
})

const SmartInput = Input({
  padding: '12px 16px',
  borderRadius: '8px',
  border: '2px solid #D1D5DB',

  css: {
    '&:focus': {
      outline: 'none',
      borderColor: '#3B82F6',
      boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)',
    },
    '&:invalid': {
      borderColor: '#EF4444',
    },
    '&:valid': {
      borderColor: '#10B981',
    },
    '&::placeholder': {
      color: '#9CA3AF',
      fontSize: '14px',
    },
  },
})

Structural Pseudo-Classes

import { Div } from '@meonode/ui'

const StyledList = Div({
  css: {
    '& .item:first-of-type': {
      borderTop: '3px solid #3B82F6',
      fontWeight: 'bold',
    },
    '& .item:last-of-type': {
      borderBottom: '3px solid #3B82F6',
    },
    '& .item:nth-of-type(odd)': {
      backgroundColor: '#F3F4F6',
    },
    '& .item:nth-of-type(even)': {
      backgroundColor: '#FFFFFF',
    },
  },
})

Pseudo-Elements

Decorative Effects

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

const DecoratedCard = Div({
  position: 'relative',
  padding: '32px',
  backgroundColor: 'white',
  borderRadius: '16px',
  overflow: 'hidden',

  css: {
    '&::before': {
      content: '""',
      position: 'absolute',
      top: 0,
      left: 0,
      right: 0,
      height: '4px',
      background: 'linear-gradient(90deg, #FF6B6B, #4ECDC4, #45B7D1, #96CEB4)',
    },
    '&::after': {
      content: '"NEW"',
      position: 'absolute',
      top: '16px',
      right: '16px',
      backgroundColor: '#EF4444',
      color: 'white',
      padding: '4px 8px',
      borderRadius: '12px',
      fontSize: '10px',
      fontWeight: 'bold',
    },
  },
})

const QuoteBlock = Div({
  position: 'relative',
  padding: '24px 48px',
  backgroundColor: '#F8FAFC',
  borderLeft: '4px solid #3B82F6',
  fontStyle: 'italic',

  css: {
    '&::before': {
      content: '"\\201C"', // Left quote
      position: 'absolute',
      top: '8px',
      left: '16px',
      fontSize: '48px',
      color: '#3B82F6',
      opacity: 0.5,
    },
    '&::after': {
      content: '"\\201D"', // Right quote
      position: 'absolute',
      bottom: '8px',
      right: '16px',
      fontSize: '48px',
      color: '#3B82F6',
      opacity: 0.5,
    },
  },
})

const GlowingButton = Button('Glow Effect', {
  padding: '12px 24px',
  backgroundColor: '#6366F1',
  color: 'white',
  borderRadius: '8px',
  position: 'relative',

  css: {
    '&::before': {
      content: '""',
      position: 'absolute',
      inset: 0,
      borderRadius: '8px',
      background: 'linear-gradient(45deg, #6366F1, #8B5CF6)',
      filter: 'blur(8px)',
      opacity: 0,
      zIndex: -1,
      transition: 'opacity 0.3s ease',
    },
    '&:hover::before': {
      opacity: 0.7,
    },
  },
})

Media Queries

Responsive Design

import { Column } from '@meonode/ui'

const ResponsiveHero = Column({
  padding: '40px 20px',
  textAlign: 'center',

  css: {
    // Mobile-first approach
    fontSize: '16px',

    // Breakpoints
    '@media (min-width: 640px)': {
      padding: '60px 40px',
      fontSize: '18px',
    },
    '@media (min-width: 768px)': {
      padding: '80px 60px',
      fontSize: '20px',
    },
    '@media (min-width: 1024px)': {
      padding: '100px 80px',
      fontSize: '22px',
    },
    '@media (min-width: 1280px)': {
      padding: '120px 100px',
      fontSize: '24px',
    },

    // Orientation
    '@media (orientation: landscape)': {
      flexDirection: 'row',
    },
    '@media (orientation: portrait)': {
      flexDirection: 'column',
    },

    // High DPI
    '@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)': {
      '& img': {
        imageRendering: 'crisp-edges',
      },
    },

    // User preferences
    '@media (prefers-color-scheme: dark)': {
      backgroundColor: '#0F172A',
      color: '#F1F5F9',
    },
    '@media (prefers-reduced-motion: reduce)': {
      '& *': {
        animation: 'none !important',
        transition: 'none !important',
      },
    },

    // Print
    '@media print': {
      backgroundColor: 'white !important',
      color: 'black !important',
      '& button': {
        display: 'none',
      },
    },
  },
})

Keyframe Animations

Basic Animations

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

const FadeInCard = Div({
  padding: '24px',
  backgroundColor: 'white',
  borderRadius: '12px',

  css: {
    '@keyframes fadeInUp': {
      '0%': {
        opacity: 0,
        transform: 'translateY(40px)',
      },
      '100%': {
        opacity: 1,
        transform: 'translateY(0)',
      },
    },
    animation: 'fadeInUp 0.8s ease-out',
  },
})

const PulsingButton = Button('Pulse', {
  padding: '16px 32px',
  backgroundColor: '#10B981',
  color: 'white',
  borderRadius: '12px',

  css: {
    '@keyframes pulse': {
      '0%, 100%': {
        transform: 'scale(1)',
        opacity: 1,
      },
      '50%': {
        transform: 'scale(1.05)',
        opacity: 0.8,
      },
    },
    animation: 'pulse 2s infinite ease-in-out',
    '&:hover': {
      animation: 'none',
      transform: 'scale(1.1)',
    },
  },
})

const Spinner = Div({
  width: '60px',
  height: '60px',
  border: '4px solid #E5E7EB',
  borderTop: '4px solid #3B82F6',
  borderRadius: '50%',

  css: {
    '@keyframes spin': {
      '0%': { transform: 'rotate(0deg)' },
      '100%': { transform: 'rotate(360deg)' },
    },
    animation: 'spin 1s linear infinite',
  },
})

Complex Animations

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

const MorphingShape = Div({
  width: '120px',
  height: '120px',

  css: {
    '@keyframes morph': {
      '0%': {
        borderRadius: '50%',
        backgroundColor: '#EF4444',
        transform: 'rotate(0deg) scale(1)',
      },
      '25%': {
        borderRadius: '25%',
        backgroundColor: '#F59E0B',
        transform: 'rotate(90deg) scale(1.2)',
      },
      '50%': {
        borderRadius: '0%',
        backgroundColor: '#10B981',
        transform: 'rotate(180deg) scale(1)',
      },
      '75%': {
        borderRadius: '25%',
        backgroundColor: '#3B82F6',
        transform: 'rotate(270deg) scale(0.8)',
      },
      '100%': {
        borderRadius: '50%',
        backgroundColor: '#EF4444',
        transform: 'rotate(360deg) scale(1)',
      },
    },
    animation: 'morph 4s ease-in-out infinite',
    '&:hover': {
      animationDuration: '1s',
    },
  },
})

const WaveLoader = Row({
  gap: '8px',
  children: Array.from({ length: 5 }, (_, i) =>
    Div({
      key: i,
      width: '20px',
      height: '60px',
      backgroundColor: '#3B82F6',
      borderRadius: '4px',

      css: {
        '@keyframes wave': {
          '0%, 40%, 100%': { transform: 'scaleY(0.4)' },
          '20%': { transform: 'scaleY(1)' },
        },
        animation: 'wave 1.2s ease-in-out infinite',
        animationDelay: `${i * 0.1}s`,
      },
    }),
  ),
})

const RippleButton = Button('Ripple', {
  position: 'relative',
  padding: '16px 32px',
  backgroundColor: '#6366F1',
  color: 'white',
  borderRadius: '12px',
  overflow: 'hidden',

  css: {
    '&::after': {
      content: '""',
      position: 'absolute',
      top: '50%',
      left: '50%',
      width: '0',
      height: '0',
      borderRadius: '50%',
      backgroundColor: 'rgba(255,255,255,0.5)',
      transform: 'translate(-50%, -50%)',
      transition: 'all 0.5s ease-out',
      opacity: 0,
    },
    '&:active::after': {
      width: '200%',
      height: '200%',
      opacity: 1,
    },
  },
})

Advanced Selectors

Nested Rules

import { Column } from '@meonode/ui'

const AdvancedContainer = Column({
  padding: '32px',
  backgroundColor: '#FFFFFF',
  borderRadius: '16px',

  css: {
    // Direct children
    '& > *': {
      marginBottom: '20px',
      transition: 'all 0.3s ease',
    },
    '& > *:last-child': {
      marginBottom: 0,
    },

    // All buttons
    '& button': {
      fontWeight: '600',
      textTransform: 'uppercase',
      letterSpacing: '0.5px',
    },
    '& button:hover': {
      transform: 'scale(1.03) translateY(-1px)',
    },

    // Attribute selectors
    '& button[data-variant="primary"]': {
      backgroundColor: '#3B82F6',
      color: 'white',
    },
    '& button[data-variant="secondary"]': {
      backgroundColor: 'transparent',
      color: '#3B82F6',
      border: '2px solid #3B82F6',
    },
    '& button[data-variant="danger"]': {
      backgroundColor: '#EF4444',
      color: 'white',
    },

    // Typography cascade
    '& h3': {
      color: '#1F2937',
      fontWeight: '700',
      fontSize: '1.5rem',
    },
    '& p': {
      color: '#6B7280',
      lineHeight: '1.6',
    },

    // Adjacent sibling
    '& h3 + p': {
      marginTop: '8px',
      fontSize: '18px',
      color: '#4B5563',
    },

    // Nth-child patterns
    '& .item:nth-of-type(odd)': {
      backgroundColor: '#F3F4F6',
      transform: 'translateX(8px)',
    },
    '& .item:nth-of-type(even)': {
      backgroundColor: '#FFFFFF',
      transform: 'translateX(-8px)',
    },
    '& .item:nth-of-type(3n):hover': {
      backgroundColor: '#DBEAFE',
      transform: 'scale(1.02)',
    },

    // Data attributes
    '& [data-highlight="true"]': {
      backgroundColor: '#FEF3C7',
      padding: '12px',
      borderRadius: '8px',
      border: '2px solid #F59E0B',
      fontWeight: 'bold',
    },
  },
})

CSS Custom Properties

Dynamic Theming

import { Div, Button, Component } from '@meonode/ui'
import { useState } from 'react'

const DynamicTheme = Component(() => {
  const [theme, setTheme] = useState('blue')

  const themes = {
    blue: { primary: '#3B82F6', secondary: '#1E40AF', accent: '#93C5FD' },
    green: { primary: '#10B981', secondary: '#047857', accent: '#6EE7B7' },
    purple: { primary: '#8B5CF6', secondary: '#6D28D9', accent: '#C4B5FD' },
  }

  return Div({
    padding: '32px',
    borderRadius: '16px',

    css: {
      // Define CSS variables
      '--primary-color': themes[theme].primary,
      '--secondary-color': themes[theme].secondary,
      '--accent-color': themes[theme].accent,
      '--shadow-color': `${themes[theme].primary}40`,

      // Use variables
      backgroundColor: 'var(--primary-color)',
      color: 'white',
      boxShadow: '0 8px 25px var(--shadow-color)',

      '& h2': {
        color: 'var(--accent-color)',
      },
      '& button': {
        backgroundColor: 'var(--secondary-color)',
        borderRadius: '8px',
        color: 'white',
        padding: '10px 20px',
        cursor: 'pointer',
        transition: 'all 0.3s ease',
      },
      '& button:hover': {
        backgroundColor: 'var(--accent-color)',
        boxShadow: '0 6px 12px var(--shadow-color)',
      },

      // Theme-specific gradients
      '&[data-theme="blue"]': {
        background: 'linear-gradient(135deg, var(--primary-color), var(--secondary-color))',
      },
      '&[data-theme="green"]': {
        background: 'radial-gradient(circle, var(--primary-color), var(--secondary-color))',
      },
      '&[data-theme="purple"]': {
        background: 'conic-gradient(var(--primary-color), var(--accent-color), var(--secondary-color))',
      },
    },

    'data-theme': theme,

    children: Object.keys(themes).map(themeName =>
      Button(themeName, {
        key: themeName,
        onClick: () => setTheme(themeName),
        css: {
          opacity: theme === themeName ? 1 : 0.7,
          transform: theme === themeName ? 'scale(1.1)' : 'scale(1)',
        },
      }),
    ),
  })
})

Reusable Styled Components

Component Factories

import { createNode } from '@meonode/ui'

const Card = createNode('div', {
  padding: '24px',
  backgroundColor: '#FFFFFF',
  borderRadius: '16px',
  boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
  transition: 'transform 0.3s ease-in-out',

  css: {
    '&:hover': {
      transform: 'translateY(-5px)',
      boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
    },
  },
})

// Usage with override
Card({
  backgroundColor: '#F3F4F6', // Override default
  children: 'Card content',
})

Best Practices

Direct Props for Simple Styles
Use padding, color, display for declarative styling.

css Prop for Complexity
Reserve for pseudo-classes, media queries, animations, nested selectors.

CSS Variables for Dynamic Theming
Runtime theme switching with CSS custom properties.

Type-Safe Development
Leverage TypeScript autocomplete to prevent CSS syntax errors.

Component Factories for Reusability
Use createNode() for styled component factories with default props.


Common Questions

Q: Can I use CSS preprocessors like Sass?
A: No need. MeoNode's nested object syntax, media queries, and vendor prefixing cover Sass/Less functionality with better type safety.

Q: How does createNode differ from Component?
A: createNode creates a MeoNode factory function requiring .render(). Component creates a React component. Use createNode for styled primitives, Component for full React components.

Q: Does the css prop support all Emotion features?
A: Yes. MeoNode's css prop uses @emotion/react directly, supporting all Emotion features including nested selectors, media queries, keyframes, and CSS custom properties.


Next Steps

  • Theming — Design system setup, semantic tokens, context-based themes
  • Framework Integration — Next.js, Vite, Remix configuration
  • FAQ — Common patterns, troubleshooting, migration strategies
  • Release Notes — Changelog, breaking changes, upgrade guides

On this page