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.
Element polymorphism (as)
Need a layout node to render as another intrinsic tag (e.g. a styled Div as <a>)? Pass as on any styled factory — styles and Emotion classes stay on the swapped element:
import { Div } from '@meonode/ui' Div({ as: 'a', href: '/docs', backgroundColor: 'theme.primary', padding: '10px 16px', children: 'Read the docs', })
Full reference, type narrowing, and limitations → Styling → Element Polymorphism (as).
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' import type { CSSProperties } from 'react' // `style` is what makes a component stylable: it tells MeoNode the component // forwards DOM props, so CSS props are offered at the call site. Without it, // CSS props are rejected — MeoNode will not style markup it cannot reach. type ChartProps = { height: number; data: number[]; style?: CSSProperties } const Chart = Component<ChartProps>(function Chart({ height, data }) { return Div({ children: `Chart with ${data.length} points at ${height}px` }) }) Node(Chart, { props: { height: 500 }, // shielded: reaches the component as a number data: [1, 2, 3, 4], // not a CSS name, passed through untouched padding: '20px', // classified as style, arrives as `className` backgroundColor: 'white', })
Rule: if a prop name matches a CSS property and you don't want it treated as style, put it inside props: { }.
Two details worth knowing. CSS props are only accepted when the component's own
props include style — that is the signal it forwards DOM props. And a
classified style arrives as a className for the component to apply: MeoNode
can add a class, but only the component can put it on its own markup.
Requires @meonode/ui 1.8.5 or later. Before 1.8.3 the shield held only while
the component element was the render root, so a shielded height came back as a
style the moment the component was used as another node's child; 1.8.4 restored
required props inside the component body.
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>(function PrimaryButton({ children, ...rest }) { return 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.
What layout components are available?
Column and Row cover vertical and horizontal flex layouts, Div is the generic container, and semantic containers like Section, Header, Footer, Main, Nav, and Form come prebuilt. All of them accept flexbox properties and theme-aware styling.
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