Rezi’s widget system is built on VNodes (virtual nodes) — lightweight plain objects that describe your UI declaratively.
VNode Concept
A VNode is a plain JavaScript object with three key properties:
type VNode = {
kind: 'text' | 'box' | 'button' | ...,
props: Record<string, unknown>,
children?: VNode[],
}
You never construct VNodes manually. Instead, use the ui.* factory functions:
import { ui } from '@rezi-ui/core';
const myTree = ui.column({ gap: 1 }, [
ui.text('Hello, World!'),
ui.button({ id: 'ok', label: 'OK' }),
]);
Each ui.* function returns a properly-typed VNode for that widget kind.
The ui namespace provides 60+ factory functions organized by category:
Container and layout widgets:
ui.box({ border: 'single', p: 1 }, children)
ui.row({ gap: 1, align: 'center' }, children)
ui.column({ gap: 2, justify: 'between' }, children)
ui.grid({ columns: 3, gap: 1 }, children)
Content Widgets
Display information:
ui.text('Hello, World!')
ui.text('Bold text', { bold: true })
ui.text('Large heading', { variant: 'heading' })
Accept user input:
ui.button({
id: 'submit',
label: 'Submit',
intent: 'primary',
onPress: () => handleSubmit(),
})
Display structured data:
ui.table({
id: 'users',
columns: [
{ key: 'name', title: 'Name' },
{ key: 'email', title: 'Email' },
],
rows: state.users,
onRowPress: user => viewDetails(user),
})
Modal interfaces:
ui.modal({
id: 'confirm-delete',
title: 'Confirm Delete',
open: state.showDeleteModal,
content: ui.column({ gap: 1 }, [
ui.text('Are you sure you want to delete this item?'),
ui.actions([
ui.button({ id: 'cancel', label: 'Cancel', intent: 'secondary' }),
ui.button({ id: 'confirm', label: 'Delete', intent: 'danger' }),
]),
]),
})
Loading and error states:
ui.spinner({ text: 'Loading...' })
ui.progress({
value: state.uploadProgress,
max: 100,
label: 'Uploading',
})
ui.skeleton({ width: 20, height: 3 })
Every widget has a corresponding props interface:
import type { ButtonProps, InputProps } from '@rezi-ui/core';
const buttonProps: ButtonProps = {
id: 'submit',
label: 'Submit',
intent: 'primary',
onPress: () => handleSubmit(),
};
const inputProps: InputProps = {
id: 'email',
value: state.email,
placeholder: 'Enter email',
onInput: value => updateEmail(value),
};
Common Prop Patterns
key for Reconciliation
When rendering dynamic lists, always provide a key prop:
ui.column(
items.map(item =>
ui.text(item.name, { key: item.id })
)
)
Without keys, Rezi cannot track which items changed, added, or removed. This leads to incorrect reconciliation and lost widget state.
id for Interactivity
All focusable widgets require an id prop:
// ✅ Correct
ui.button({ id: 'submit', label: 'Submit' })
ui.input({ id: 'email', value: '' })
// ❌ Runtime error
ui.button({ label: 'Submit' }) // Missing id!
The id is used for:
- Focus management: Tab/Shift+Tab navigation
- Event routing: Which button was pressed
- Focus restoration: After modal closes
Styling Props
Many widgets accept a style prop for visual customization:
import type { TextStyle } from '@rezi-ui/core';
const style: TextStyle = {
fg: 'blue', // Foreground color
bg: 'white', // Background color
bold: true, // Bold text
italic: false,
underline: false,
strikethrough: false,
};
ui.text('Styled text', { style });
Layout Constraint Props
Control widget dimensions:
ui.box({
width: 40, // Fixed width (cells)
height: 10, // Fixed height (cells)
minWidth: 20, // Minimum width
maxWidth: 60, // Maximum width
flex: 1, // Flex grow factor
flexShrink: 0, // Disable shrinking
flexBasis: 30, // Initial flex size
})
Spacing Props
Padding and margin:
ui.box({
p: 2, // All sides
px: 3, // Horizontal (left + right)
py: 1, // Vertical (top + bottom)
pt: 1, // Top only
pr: 2, // Right only
pb: 1, // Bottom only
pl: 2, // Left only
})
Accepts numbers (cells) or spacing keys ('xs', 'sm', 'md', 'lg', 'xl', '2xl').
Basic Composition Patterns
Nested Containers
ui.page({ p: 1 }, [
ui.panel('User Profile', [
ui.column({ gap: 1 }, [
ui.text(state.user.name, { variant: 'heading' }),
ui.text(state.user.email, { variant: 'caption' }),
ui.divider(),
ui.text(state.user.bio),
]),
]),
])
Conditional Rendering
import { show, when } from '@rezi-ui/core';
// Simple conditional
ui.column([
show(state.isLoggedIn, () => ui.text('Welcome back!')),
when(state.error, error => ui.errorDisplay(error)),
])
// Multiple branches
import { match } from '@rezi-ui/core';
match(state.status, {
'loading': () => ui.spinner({ text: 'Loading...' }),
'success': () => ui.text('Success!'),
'error': () => ui.errorDisplay('Failed'),
})
List Rendering
import { each } from '@rezi-ui/core';
ui.column(
each(state.items, (item, i) =>
ui.row({ gap: 1, key: item.id }, [
ui.text(`${i + 1}.`),
ui.text(item.name),
ui.button({
id: `delete-${item.id}`,
label: 'Delete',
intent: 'danger',
}),
])
)
)
Reusable Fragments
function renderUserCard(user: User) {
return ui.box({ border: 'rounded', p: 1 }, [
ui.text(user.name, { variant: 'heading' }),
ui.text(user.email, { variant: 'caption' }),
]);
}
app.view(state =>
ui.column(
state.users.map(user => renderUserCard(user))
)
);
For components with local state, use defineWidget() instead of plain functions. See Composition.
Design System Integration
Rezi provides design system props for consistent styling:
ui.button({
id: 'submit',
label: 'Submit',
dsVariant: 'solid', // solid | soft | outline | ghost
dsTone: 'primary', // primary | secondary | success | danger | warning
dsSize: 'md', // sm | md | lg
})
// Or use intent shortcuts
ui.button({
id: 'submit',
label: 'Submit',
intent: 'primary', // Maps to dsVariant + dsTone
})
| Intent | Maps to | Use for |
|---|
primary | solid + primary | Main CTA (Save, Submit) |
secondary | soft + default | Secondary actions (Cancel, Back) |
danger | outline + danger | Destructive actions (Delete) |
success | soft + success | Positive confirmations |
warning | soft + warning | Caution actions |
link | ghost + default | Minimal/link-style actions |
Next Steps
Composition
Build reusable components with defineWidget
Widget Catalog
Explore all 60+ built-in widgets
Layout
Learn about the layout engine
Theming
Customize colors and spacing