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

Usage — MeoNode UI

Build type-safe React UIs using function composition instead of JSX. This guide covers the core patterns of @meonode/ui.


Node Fundamentals

MeoNode uses node functions (Column, H1, Button, Text, etc.) as building blocks. Each represents an HTML element or component with type-safe props.

Children-first Nodes

Nodes for content-led elements take children as the first argument and an optional props object as the second.

Applies to: H1H6, Text, Span, Button, A, Label, Code, etc.

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

H1('Welcome', { fontSize: '2.5rem', color: '#333' })
Text('Build UIs with function composition.', { color: '#666' })
Button('Learn More', {
  padding: '10px 20px',
  backgroundColor: 'dodgerblue',
  color: 'white',
  onClick: () => alert('Hello MeoNode!'),
})

The first argument can also be an array of nodes:

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

Button([Span('🚀', { marginRight: 8 }), 'Launch'], { padding: '10px 20px' })

Props-first Nodes

Layout and container elements take a single props object; children go on the children prop.

Applies to: Column, Row, Div, Section, Header, Footer, Main, Nav, Form, Ul, Ol

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

Column({
  padding: 20,
  gap: 12,
  children: [
    H1('Dashboard'),
    Text('Container nodes pass children via props.'),
    Row({
      gap: 10,
      children: [
        Button('Save', { backgroundColor: 'dodgerblue', color: 'white' }),
        Button('Cancel', { backgroundColor: '#ccc' }),
      ],
    }),
  ],
})

Notes:

  • Numbers passed to CSS props are interpreted as pixels (2020px).
  • Standard React event handlers (onClick, onChange, onMouseEnter, …) are passed as props.
  • Column is flex-direction: column, Row is flex-direction: row.

Styling

CSS properties pass directly as props — no separate stylesheet, no style={{ ... }} wrapper. Powered by @emotion/react under the hood.

css prop for advanced selectors

Pseudo-classes, media queries, and nested selectors go in the css prop:

import { Button } from '@meonode/ui'

Button('Hover Me', {
  padding: '12px 24px',
  backgroundColor: '#3B82F6',
  css: {
    '&:hover': {
      transform: 'scale(1.05)',
      boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)',
    },
    '@media (max-width: 768px)': {
      padding: '8px 16px',
    },
  },
})

For more, see the Styling guide.

Bypassing CSS processing

When a custom component takes a prop whose name matches a CSS property (e.g. height for chart dimensions), wrap it in props so the styling engine ignores it:

import { Node, Component, Div } from '@meonode/ui'

type ChartProps = { height: number; data: number[] }

const Chart = Component<ChartProps>(({ height, data }) =>
  Div({ children: `Chart with ${data.length} points at ${height}px` }),
)

Node(Chart, {
  props: { height: 500 }, // forwarded as logic, not styled
  data: [1, 2, 3, 4],     // non-CSS prop, stays at root
  padding: '20px',        // styled (container)
  backgroundColor: 'white',
})

Rule: if a prop name matches a CSS property and you don't want it treated as style, put it inside props: { }.


Conditional Rendering

Use standard JavaScript conditionals:

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

const AuthStatus = () => {
  const [isLoggedIn, setIsLoggedIn] = useState(false)

  return Column({
    children: isLoggedIn
      ? Text('Welcome back!', { color: 'green' })
      : Button('Login', {
          backgroundColor: 'blue',
          color: 'white',
          padding: 10,
          onClick: () => setIsLoggedIn(true),
        }),
  }).render()
}

Hook-using components

Calling a hook-using component directly inside a conditional violates the Rules of Hooks. Wrap it with Node() to give React a real component boundary:

showMore && Node(DetailComponent, { info: 'Conditionally visible' })

The full pattern (with inline-function and Component HOC alternatives) is covered in the conditional hooks FAQ.


Lists

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

const items = [
  { id: 1, text: 'First' },
  { id: 2, text: 'Second' },
  { id: 3, text: 'Third' },
]

Column({
  children: items.map(item =>
    Text(item.text, { key: item.id, padding: 8, backgroundColor: '#e9e9e9' }),
  ),
})

Performance: Node Memoization

Pass a dependency array as the second argument to memoize a node. An empty array ([]) makes a node static — render once, never update:

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

const App = () => {
  const [count, setCount] = useState(0)

  return Div({
    onClick: () => setCount(count + 1),
    children: [
      Div({ children: `Reactive: ${count}` }),    // updates every click
      Div({ children: `Static: ${count}` }, []),   // stays at 0
    ],
  }).render()
}

With dependencies, the node updates only when those values change:

Div({ children: `Count 1: ${count1}` }, [count1]) // updates on count1 only
Div({ children: `Count 2: ${count2}` }, [count2]) // updates on count2 only

The same [] / dep-array pattern works with Component-wrapped functions: MyHocComponent(props, deps).


Integrating JSX / React components

Wrap any React component with Node() to use it as a MeoNode element. Props, hooks, refs, and event handlers all work unchanged.

import { Node, Column, H1 } from '@meonode/ui'
import { TextField } from '@mui/material'

Column({
  children: [
    H1('Form'),
    Node(TextField, { label: 'Name', variant: 'outlined', fullWidth: true }),
  ],
})

Components used with Node() must return a ReactElement

Don't forget .render() on MeoNode functions you'll pass to Node():

const Bad = () => Column({ children: 'oops' })            // returns Node instance
const Good = () => Column({ children: 'ok' }).render()    // returns ReactElement

Node(Good)   // ✅
Node(Bad)    // ❌ React error

JSX components already return ReactElements, so they work as-is.

Reusable factories

For components used repeatedly with default props, use createNode (props-first) or createChildrenFirstNode (children-first):

import { createNode, createChildrenFirstNode, Column } from '@meonode/ui'
import { TextField, Button as MuiButton } from '@mui/material'

const StyledTextField = createNode(TextField, { variant: 'outlined', fullWidth: true })
const PrimaryButton = createChildrenFirstNode(MuiButton, { variant: 'contained' })

Column({
  gap: 16,
  children: [
    StyledTextField({ label: 'Username' }),
    StyledTextField({ label: 'Password', type: 'password' }),
    PrimaryButton('Login', { onClick: () => login() }),
  ],
})

See Node vs createNode vs createChildrenFirstNode for when to use each.


Reusable Components

Use the Component factory to encapsulate logic and styling:

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

interface PrimaryButtonProps {
  onClick: () => void
}

const PrimaryButton = Component<PrimaryButtonProps>(({ children, ...rest }) =>
  Button(children, {
    padding: '12px 24px',
    backgroundColor: 'darkgreen',
    color: 'white',
    borderRadius: 8,
    cursor: 'pointer',
    fontSize: '1.1rem',
    ...rest, // additional props (excluding children to avoid double-pass)
  }),
)

PrimaryButton({ children: 'Submit', onClick: () => console.log('Submitted!') })

Destructure children separately from rest so it isn't passed to Button twice.


Complete Example

A small Todo app combining the patterns above:

import { Column, Row, H1, Text, Button, Input, Node } from '@meonode/ui'
import { useState } from 'react'

interface Todo {
  id: number
  text: string
  completed: boolean
}

const TodoItem = ({
  todo,
  onToggle,
  onDelete,
}: {
  todo: Todo
  onToggle: (id: number) => void
  onDelete: (id: number) => void
}) =>
  Row({
    padding: 12,
    backgroundColor: todo.completed ? '#f0f8f0' : '#fff',
    borderRadius: 8,
    border: '1px solid #e0e0e0',
    alignItems: 'center',
    gap: 12,
    children: [
      Text(todo.text, {
        flex: 1,
        textDecoration: todo.completed ? 'line-through' : 'none',
        color: todo.completed ? '#666' : '#333',
      }),
      Button('Toggle', {
        padding: '6px 12px',
        backgroundColor: todo.completed ? '#28a745' : '#6c757d',
        color: 'white',
        borderRadius: 4,
        onClick: () => onToggle(todo.id),
      }),
      Button('Delete', {
        padding: '6px 12px',
        backgroundColor: '#dc3545',
        color: 'white',
        borderRadius: 4,
        onClick: () => onDelete(todo.id),
      }),
    ],
  }).render()

export default function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([
    { id: 1, text: 'Learn MeoNode UI', completed: false },
  ])
  const [input, setInput] = useState('')

  const addTodo = () => {
    if (!input.trim()) return
    setTodos([...todos, { id: Date.now(), text: input.trim(), completed: false }])
    setInput('')
  }

  const toggleTodo = (id: number) =>
    setTodos(todos.map(t => (t.id === id ? { ...t, completed: !t.completed } : t)))

  const deleteTodo = (id: number) =>
    setTodos(todos.filter(t => t.id !== id))

  return Column({
    padding: 40,
    maxWidth: 600,
    margin: '0 auto',
    children: [
      H1('MeoNode Todo App', { textAlign: 'center', marginBottom: 30 }),
      Row({
        gap: 12,
        marginBottom: 24,
        children: [
          Input({
            value: input,
            onChange: e => setInput(e.target.value),
            placeholder: 'Add a new todo...',
            padding: '12px 16px',
            borderRadius: 8,
            border: '2px solid #e0e0e0',
            flex: 1,
            onKeyDown: e => e.key === 'Enter' && addTodo(),
          }),
          Button('Add', {
            padding: '12px 24px',
            backgroundColor: '#007bff',
            color: 'white',
            borderRadius: 8,
            onClick: addTodo,
          }),
        ],
      }),
      Column({
        gap: 12,
        children:
          todos.length > 0
            ? todos.map(todo =>
                Node(TodoItem, { key: todo.id, todo, onToggle: toggleTodo, onDelete: deleteTodo }),
              )
            : [Text('No todos yet. Add one above!', { textAlign: 'center', color: '#666' })],
      }),
    ],
  }).render()
}

How do node functions work?

Node functions are typed factory calls that return node objects. Content-first nodes take children first, while layout nodes use props-first with children inside the props object.

How do I create reusable components?

Use Component when the unit contains logic and hooks, and use createNode / createChildrenFirstNode for reusable styled wrappers.

How do I handle conditional rendering and lists?

Use normal JavaScript (if, ternary, map). For hook-using units, keep stable component boundaries with Node() or Component.

How do I handle events and interactions?

Attach standard React handlers (onClick, onChange, etc.) directly in node props. They are forwarded to DOM or wrapped components as expected.

More details: /docs/getting-started/faq


Next Steps

  • Styling — Pseudo-classes, media queries, animations
  • Theming — Design tokens and the MeoTheme augmentation
  • Framework Integration — Next.js, Vite, Remix
  • FAQ — Common patterns, edge cases, troubleshooting

On this page