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: H1–H6, 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 (
20→20px). - Standard React event handlers (
onClick,onChange,onMouseEnter, …) are passed as props. Columnisflex-direction: column,Rowisflex-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() }
Related FAQ
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
MeoThemeaugmentation - Framework Integration — Next.js, Vite, Remix
- FAQ — Common patterns, edge cases, troubleshooting
On this page
- Usage — MeoNode UI