Skip to main content
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.

Widget Factory Functions

The ui namespace provides 60+ factory functions organized by category:

Structural Widgets

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' })

Interactive Widgets

Accept user input:
ui.button({
  id: 'submit',
  label: 'Submit',
  intent: 'primary',
  onPress: () => handleSubmit(),
})

Data Widgets

Display structured data:
ui.table({
  id: 'users',
  columns: [
    { key: 'name', title: 'Name' },
    { key: 'email', title: 'Email' },
  ],
  rows: state.users,
  onRowPress: user => viewDetails(user),
})

Overlay Widgets

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' }),
    ]),
  ]),
})

Feedback Widgets

Loading and error states:
ui.spinner({ text: 'Loading...' })

ui.progress({
  value: state.uploadProgress,
  max: 100,
  label: 'Uploading',
})

ui.skeleton({ width: 20, height: 3 })

Widget Props and Types

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
})

Button Intent Shortcuts

IntentMaps toUse for
primarysolid + primaryMain CTA (Save, Submit)
secondarysoft + defaultSecondary actions (Cancel, Back)
dangeroutline + dangerDestructive actions (Delete)
successsoft + successPositive confirmations
warningsoft + warningCaution actions
linkghost + defaultMinimal/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

Build docs developers (and LLMs) love