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' })
Element Polymorphism (as)
Swap the rendered HTML tag while keeping the node's styles and Emotion class — similar to Emotion's as prop. Useful when a layout factory should occasionally render as an anchor, span, or other intrinsic element.
import { Div, P, Node } from '@meonode/ui' // Styled block that renders as a link Div({ as: 'a', href: '/home', backgroundColor: 'theme.primary', color: 'theme.primary.content', padding: '12px 24px', borderRadius: 8, children: 'Go home', }) // Children-first factories accept `as` in the props object P('Badge', { as: 'span', fontSize: 12, color: 'theme.primary' }) // Low-level Node() factory Node('div', { as: 'a', href: '#', color: 'blue', children: 'Link' })
Works on Node(), createNode(), and createChildrenFirstNode(). TypeScript narrows DOM props and event handlers to the resolved tag — href is valid when as: 'a', and onClick types e.currentTarget as HTMLAnchorElement.
When to use as vs a dedicated factory
| Approach | Use when |
|---|---|
Div({ as: 'a', … }) | You already have styled defaults on a factory and only need a different tag occasionally |
createNode('a', { … }) | Links (or other tags) are a first-class reusable component in your design system |
as is stripped at render time and never appears as a DOM attribute.
Limitations
- Intrinsic HTML tags only — custom React components cannot be used as
astargets. Wrap withNode(MyComponent, …)or use that component's own factory instead. - Not available on no-style tags — elements like
Scriptthat skip the styling pipeline do not acceptas. - RSC-safe — the swap runs through the same Emotion compilation path on server and client, so critical CSS stays consistent across SSR and hydration.
Related FAQ
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.
How do I render a styled node as a different HTML tag?
Use the as prop on any styled factory — for example Div({ as: 'a', href: '…', … }). See Element Polymorphism (as) above.
More details: /docs/getting-started/faq
Next Steps
- Theming — Design tokens and the
MeoThemeaugmentation - Framework Integration — Next.js, Vite, Remix
- FAQ — Common patterns and edge cases
On this page
- Styling Guide — MeoNode UI