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

Styling Guide — MeoNode UI

Type-safe, theme-aware CSS-in-JS powered by @emotion/react. Apply CSS as props, or use the css prop for pseudo-classes, media queries, keyframes, and nested selectors.


Fundamentals

CSS properties pass directly as props on any node:

import { Button } from '@meonode/ui'

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

For anything beyond plain properties — selectors, queries, animations — use the css prop:

import { Div } from '@meonode/ui'

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

Numbers are interpreted as pixels. Vendor prefixing, dedup, and SSR critical-CSS extraction happen automatically through Emotion.


Pseudo-Classes & Pseudo-Elements

Use selectors inside css. The & token refers to the current node.

import { Button } from '@meonode/ui'

Button('Hover Me', {
  padding: '14px 28px',
  backgroundColor: '#3B82F6',
  color: 'white',
  borderRadius: 10,
  transition: 'all 0.2s ease',
  css: {
    '&:hover': { backgroundColor: '#2563EB', transform: 'translateY(-2px)' },
    '&:active': { transform: 'translateY(0)' },
    '&:focus-visible': { outline: '2px solid #1D4ED8', outlineOffset: 2 },
    '&:disabled': { backgroundColor: '#9CA3AF', cursor: 'not-allowed' },
    '&::before': {
      content: '"→ "',
      marginRight: 4,
    },
  },
})

Structural pseudo-classes (:first-of-type, :nth-of-type, ::placeholder, etc.) work the same way.


Media Queries

Standard at-rules go in css:

import { Column } from '@meonode/ui'

Column({
  padding: '40px 20px',
  fontSize: 16,
  css: {
    '@media (min-width: 768px)': { padding: '60px 40px', fontSize: 18 },
    '@media (min-width: 1024px)': { padding: '80px 60px', fontSize: 20 },

    // User preference queries also work
    '@media (prefers-color-scheme: dark)': {
      backgroundColor: '#0F172A',
      color: '#F1F5F9',
    },
    '@media (prefers-reduced-motion: reduce)': {
      transition: 'none',
    },
  },
})

Keyframe Animations

Define @keyframes inside css and reference the name in animation:

import { Div } from '@meonode/ui'

const Spinner = Div({
  width: 60,
  height: 60,
  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',
  },
})

For staggered effects, set animationDelay per element:

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

const WaveLoader = Row({
  gap: 8,
  children: Array.from({ length: 5 }, (_, i) =>
    Div({
      key: i,
      width: 20,
      height: 60,
      backgroundColor: '#3B82F6',
      borderRadius: 4,
      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`,
      },
    }),
  ),
})

Nested & Attribute Selectors

import { Column } from '@meonode/ui'

Column({
  padding: 32,
  borderRadius: 16,
  css: {
    // Direct children
    '& > *': { marginBottom: 20 },
    '& > *:last-child': { marginBottom: 0 },

    // All buttons inside
    '& button': { fontWeight: 600 },
    '& button[data-variant="primary"]': {
      backgroundColor: '#3B82F6',
      color: 'white',
    },

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

CSS Variables

Define custom properties in css and reference them with var(). Useful for runtime theme swapping without re-rendering subtree styles:

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

const themes = {
  blue: { primary: '#3B82F6', accent: '#93C5FD' },
  green: { primary: '#10B981', accent: '#6EE7B7' },
}

const ThemedCard = Component(() => {
  const [theme, setTheme] = useState<keyof typeof themes>('blue')

  return Div({
    padding: 32,
    borderRadius: 16,
    css: {
      '--primary': themes[theme].primary,
      '--accent': themes[theme].accent,
      backgroundColor: 'var(--primary)',
      color: 'white',
      '& button': {
        backgroundColor: 'var(--accent)',
        color: 'white',
        padding: '8px 16px',
        borderRadius: 8,
      },
    },
    children: Object.keys(themes).map(name =>
      Button(name, {
        key: name,
        onClick: () => setTheme(name as keyof typeof themes),
      }),
    ),
  })
})

For static design tokens, prefer the theming system over raw CSS variables.


Reusable Styled Components

Use createNode (props-first) or createChildrenFirstNode (children-first) to bake in default styles:

import { createNode } from '@meonode/ui'

const Card = createNode('div', {
  padding: 24,
  backgroundColor: '#FFFFFF',
  borderRadius: 16,
  boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
  css: {
    '&:hover': { transform: 'translateY(-4px)' },
  },
})

// Override defaults at the call site
Card({ backgroundColor: '#F3F4F6', children: 'Card content' })

How does styling work in MeoNode UI?

Styling is prop-driven and theme-aware. Use direct CSS props for straightforward styling, and use the css prop for selectors, media queries, keyframes, and nested rules.

How do I pass props like height to custom components?

If a prop name overlaps with CSS properties but should be treated as component logic, place it under props so it is forwarded without being interpreted as style.

How does the theming system work?

Use ThemeProvider and token paths like theme.primary so style values stay centralized and consistent across components.

More details: /docs/getting-started/faq


Next Steps

On this page