---
title: Widget Contract
description: Standard props, events, and lifecycle for ObjectUI components
---

import { Component, Zap, Code, Check, AlertCircle, Globe } from 'lucide-react';


The **Widget Contract** defines the standard interface that all ObjectUI components must implement. This contract ensures consistency across renderers and enables components to be composed, configured, and extended predictably.

## Philosophy: Props Down, Events Up

ObjectUI follows React's unidirectional data flow pattern:

```
┌─────────────────────────────────────┐
│         Parent Component            │
│  (Form, Section, Container)         │
└─────────────┬───────────────────────┘
              │
              │ Props ↓ (data, config, callbacks)
              │
┌─────────────▼───────────────────────┐
│       Child Component               │
│  (TextField, Select, Button)        │
└─────────────┬───────────────────────┘
              │
              │ Events ↑ (onChange, onClick, onBlur)
              │
┌─────────────▼───────────────────────┐
│         Parent Component            │
│  (Updates state, triggers actions)  │
└─────────────────────────────────────┘
```

**Key Principles:**
- **Props are immutable:** Components receive props, never modify them
- **Events are declarative:** Components emit events, parent handles them
- **State is lifted:** Form state lives in parent, not in field components

## Standard Component Props

Every ObjectUI component receives a standard set of base props.

### BaseComponentProps

```typescript
interface BaseComponentProps {
  // Identification
  id: string;                    // Unique component ID
  name: string;                  // Field name (for form submission)
  
  // Display
  label?: string;                // Display label
  placeholder?: string;          // Placeholder text
  helpText?: string;             // Helper text below field
  tooltip?: string;              // Tooltip on hover
  icon?: string;                 // Lucide icon name
  
  // State
  value?: any;                   // Current value
  defaultValue?: any;            // Initial value
  disabled?: boolean;            // Disabled state
  readonly?: boolean;            // Read-only state
  required?: boolean;            // Required field marker
  
  // Validation
  error?: string;                // Error message
  warning?: string;              // Warning message
  valid?: boolean;               // Validation state
  
  // Layout
  className?: string;            // CSS classes
  style?: CSSProperties;         // Inline styles
  span?: number;                 // Grid column span (1-12)
  hidden?: boolean;              // Visibility
  
  // Accessibility
  ariaLabel?: string;            // ARIA label
  ariaDescribedBy?: string;      // ARIA description
  tabIndex?: number;             // Tab order
  
  // Events
  onChange?: (value: any) => void;
  onBlur?: () => void;
  onFocus?: () => void;
  onClick?: () => void;
}
```

### Example: TextField Component

```yaml
# YAML Configuration
field:
  name: customer_email
  type: email
  label: Email Address
  placeholder: you@company.com
  helpText: We'll never share your email
  required: true
  icon: mail
```

**Rendered as (React):**
```tsx
<TextField
  id="field_customer_email"
  name="customer_email"
  type="email"
  label="Email Address"
  placeholder="you@company.com"
  helpText="We'll never share your email"
  required={true}
  icon="mail"
  value={formData.customer_email}
  error={errors.customer_email}
  onChange={(value) => setFormData({ ...formData, customer_email: value })}
  onBlur={() => validateField('customer_email')}
/>
```

## Field-Specific Props

Different field types have specialized props.

### TextField Props

```typescript
interface TextFieldProps extends BaseComponentProps {
  type: 'text' | 'email' | 'tel' | 'url' | 'password';
  maxLength?: number;           // Character limit
  minLength?: number;           // Minimum characters
  pattern?: string;             // Regex validation pattern
  autocomplete?: string;        // Browser autocomplete hint
  spellcheck?: boolean;         // Enable spell check
}
```

**Example:**
```yaml
field:
  name: company_name
  type: text
  maxLength: 100
  autocomplete: organization
  spellcheck: true
```

### TextArea Props

```typescript
interface TextAreaProps extends BaseComponentProps {
  rows?: number;                // Visible rows
  maxLength?: number;           // Character limit
  resize?: 'none' | 'vertical' | 'horizontal' | 'both';
  autoGrow?: boolean;           // Auto-expand on content
}
```

**Example:**
```yaml
field:
  name: description
  type: textarea
  rows: 5
  maxLength: 1000
  autoGrow: true
  resize: vertical
```

### Select Props

```typescript
interface SelectProps extends BaseComponentProps {
  options: Option[];            // Available options
  multiple?: boolean;           // Multi-select
  searchable?: boolean;         // Filterable options
  clearable?: boolean;          // Show clear button
  placeholder?: string;         // Placeholder when empty
  
  // Async options
  optionsSource?: string;       // URL to fetch options
  optionsDependsOn?: string[];  // Refresh when these fields change
}

interface Option {
  value: string | number;
  label: string;
  icon?: string;
  disabled?: boolean;
  group?: string;               // Option group
}
```

**Example:**
```yaml
field:
  name: country
  type: select
  options:
    - { value: US, label: United States, icon: flag-us }
    - { value: CA, label: Canada, icon: flag-ca }
    - { value: UK, label: United Kingdom, icon: flag-gb }
  searchable: true
  clearable: true
```

### Lookup Props (Reference Field)

```typescript
interface LookupProps extends BaseComponentProps {
  object: string;               // Target object name
  displayField: string;         // Field to display (e.g., 'name')
  searchFields: string[];       // Fields to search
  filters?: FilterExpression;   // Pre-filter options
  multiple?: boolean;           // Multi-select
  createNew?: boolean;          // Allow creating new records
  
  // Modal configuration
  modalTitle?: string;
  modalColumns?: string[];      // Columns to show in picker modal
}
```

**Example:**
```yaml
field:
  name: account_id
  type: lookup
  object: account
  displayField: name
  searchFields: [name, email, phone]
  filters:
    status: active
  createNew: true
  modalTitle: Select Account
  modalColumns: [name, industry, city]
```

### Boolean Props (Checkbox/Toggle)

```typescript
interface BooleanProps extends BaseComponentProps {
  variant: 'checkbox' | 'toggle' | 'switch';
  labelPosition?: 'left' | 'right';
  trueLabel?: string;           // Label when true (for toggles)
  falseLabel?: string;          // Label when false
}
```

**Example:**
```yaml
field:
  name: email_notifications
  type: boolean
  variant: toggle
  trueLabel: Enabled
  falseLabel: Disabled
  labelPosition: right
```

### Date/DateTime Props

```typescript
interface DateProps extends BaseComponentProps {
  type: 'date' | 'datetime' | 'time';
  format?: string;              // Display format (e.g., 'MM/DD/YYYY')
  min?: string;                 // Minimum date
  max?: string;                 // Maximum date
  disabledDates?: string[];     // Specific disabled dates
  showTime?: boolean;           // Include time picker (for datetime)
  timeFormat?: '12h' | '24h';   // Time format
}
```

**Example:**
```yaml
field:
  name: meeting_date
  type: datetime
  format: MM/DD/YYYY hh:mm A
  min: today
  showTime: true
  timeFormat: 12h
```

### Number/Currency Props

```typescript
interface NumberProps extends BaseComponentProps {
  type: 'number' | 'currency' | 'percent';
  min?: number;                 // Minimum value
  max?: number;                 // Maximum value
  step?: number;                // Increment step
  precision?: number;           // Decimal places
  
  // Currency-specific
  currency?: string;            // ISO currency code (USD, EUR)
  currencyDisplay?: 'symbol' | 'code' | 'name';
  
  // Percent-specific
  percentPrecision?: number;    // Decimal places for percent
}
```

**Example:**
```yaml
field:
  name: unit_price
  type: currency
  currency: USD
  min: 0
  precision: 2
  currencyDisplay: symbol
```

## Standard Events

All components emit standard lifecycle and interaction events.

### Core Events

```typescript
interface ComponentEvents {
  // Value changes
  onChange: (value: any, context: EventContext) => void;
  onInput: (value: any) => void;  // Real-time input (every keystroke)
  
  // Focus events
  onFocus: (context: EventContext) => void;
  onBlur: (context: EventContext) => void;
  
  // Interaction events
  onClick: (context: EventContext) => void;
  onDoubleClick: (context: EventContext) => void;
  
  // Validation events
  onValidate: (value: any) => ValidationResult;
  onError: (error: ValidationError) => void;
  
  // Lifecycle events
  onMount: () => void;
  onUnmount: () => void;
}

interface EventContext {
  fieldName: string;
  value: any;
  previousValue: any;
  formData: Record<string, any>;
  metadata: Record<string, any>;
}
```

### Event Examples

**onChange: Field Value Changed**
```yaml
field:
  name: quantity
  type: number
  onChange:
    - action: calculate_total
      formula: quantity * unit_price
      targetField: total_price
```

**onBlur: Field Loses Focus**
```yaml
field:
  name: email
  type: email
  onBlur:
    - action: validate_email
    - action: check_duplicates
      endpoint: /api/customers/check-email
```

**Conditional Logic on Change**
```yaml
field:
  name: shipping_required
  type: boolean
  onChange:
    - when: { value: true }
      show: [shipping_address, shipping_method]
    - when: { value: false }
      hide: [shipping_address, shipping_method]
      clear: [shipping_address, shipping_method]
```

## Validation Contract

Components support declarative validation rules.

### Built-in Validators

```typescript
interface FieldValidation {
  // Presence
  required?: boolean | string;   // true or custom error message
  
  // String validation
  minLength?: number;
  maxLength?: number;
  pattern?: RegExp;
  email?: boolean;
  url?: boolean;
  
  // Number validation
  min?: number;
  max?: number;
  integer?: boolean;
  positive?: boolean;
  
  // Async validation
  asyncValidator?: {
    endpoint: string;
    debounce?: number;          // Debounce delay (ms)
  };
  
  // Custom validation
  custom?: (value: any, formData: any) => ValidationResult;
}

interface ValidationResult {
  valid: boolean;
  error?: string;
  warning?: string;
}
```

### Validation Examples

**Required Field**
```yaml
field:
  name: customer_name
  type: text
  required: Customer name is required
```

**Pattern Validation**
```yaml
field:
  name: phone
  type: tel
  pattern: ^\+?[1-9]\d{1,14}$
  patternMessage: Please enter a valid phone number (E.164 format)
```

**Async Validation (Check Uniqueness)**
```yaml
field:
  name: username
  type: text
  asyncValidator:
    endpoint: /api/users/check-username
    debounce: 500  # Wait 500ms after typing stops
    errorMessage: Username already taken
```

**Cross-Field Validation**
```yaml
field:
  name: password_confirm
  type: password
  custom:
    validate: password_confirm === password
    errorMessage: Passwords must match
```

## Rendering Contract

Renderers must implement specific methods to paint components.

### Renderer Interface

```typescript
interface ObjectUIRenderer {
  // Core rendering
  renderField(field: FieldDefinition): ReactNode;
  renderSection(section: SectionDefinition): ReactNode;
  renderForm(form: FormDefinition): ReactNode;
  
  // Component registry
  registerComponent(type: string, component: ComponentClass): void;
  getComponent(type: string): ComponentClass | undefined;
  
  // Lifecycle hooks
  beforeRender(definition: ComponentDefinition): void;
  afterRender(element: ReactNode): void;
  
  // Error handling
  renderError(error: Error, definition: ComponentDefinition): ReactNode;
  renderFallback(type: string): ReactNode;  // Unknown component type
}
```

### Component Registration

Custom components can be registered with the renderer:

```typescript
// Define custom component
const CustomRatingField: React.FC<BaseComponentProps> = ({ value, onChange }) => {
  return (
    <div className="rating-field">
      {[1, 2, 3, 4, 5].map(star => (
        <Star
          key={star}
          filled={value >= star}
          onClick={() => onChange(star)}
        />
      ))}
    </div>
  );
};

// Register with renderer
renderer.registerComponent('rating', CustomRatingField);
```

**Use in Layout:**
```yaml
field:
  name: customer_satisfaction
  type: rating  # ← Custom type
  label: How satisfied are you?
  min: 1
  max: 5
```

## Theme Contract

Components must support theme customization.

### Theme Props

```typescript
interface ThemeProps {
  // Colors
  colorScheme?: 'light' | 'dark' | 'auto';
  primaryColor?: string;
  secondaryColor?: string;
  errorColor?: string;
  warningColor?: string;
  successColor?: string;
  
  // Typography
  fontFamily?: string;
  fontSize?: 'small' | 'medium' | 'large';
  
  // Spacing
  density?: 'compact' | 'default' | 'spacious';
  
  // Borders
  borderRadius?: number;        // px
  borderStyle?: 'solid' | 'dashed' | 'none';
  
  // Variants
  variant?: 'outlined' | 'filled' | 'standard';
}
```

### Theme Example

```yaml
theme:
  colorScheme: light
  primaryColor: '#3B82F6'
  errorColor: '#EF4444'
  fontFamily: Inter, sans-serif
  fontSize: medium
  density: default
  borderRadius: 8
  variant: outlined

fields:
  - name: email
    type: email
    # Inherits theme automatically
```

## Accessibility Contract

All components must meet WCAG 2.1 AA standards.

### Required Accessibility Features

```typescript
interface A11yProps {
  // ARIA attributes
  role?: string;                // ARIA role
  ariaLabel?: string;           // Accessible label
  ariaDescribedBy?: string;     // ID of description element
  ariaLive?: 'polite' | 'assertive' | 'off';
  ariaAtomic?: boolean;
  
  // Keyboard navigation
  tabIndex?: number;            // Tab order (-1 = not tabbable)
  onKeyDown?: (e: KeyboardEvent) => void;
  onKeyUp?: (e: KeyboardEvent) => void;
  
  // Focus management
  autoFocus?: boolean;          // Auto-focus on mount
  focusOnError?: boolean;       // Focus when error occurs
}
```

### Accessibility Example

```yaml
field:
  name: credit_card
  type: text
  label: Credit Card Number
  ariaLabel: Credit card number, 16 digits
  ariaDescribedBy: credit_card_help
  helpText:
    id: credit_card_help
    text: Enter 16-digit card number without spaces
  autocomplete: cc-number
  inputMode: numeric
```

**Keyboard Support (Required):**
- **Tab:** Move to next field
- **Shift+Tab:** Move to previous field
- **Enter:** Submit form (on last field)
- **Escape:** Cancel/clear (if applicable)
- **Arrow keys:** Navigate options (Select, Radio)
- **Space:** Toggle (Checkbox, Toggle)

## Performance Contract

Components should optimize rendering and minimize re-renders.

### Optimization Guidelines

```typescript
interface PerformanceProps {
  // Lazy loading
  lazy?: boolean;               // Defer rendering until visible
  lazyPlaceholder?: ReactNode;  // Show while loading
  
  // Memoization
  memoize?: boolean;            // Cache rendered output
  memoizeKey?: string[];        // Re-render only when these props change
  
  // Virtualization
  virtualScroll?: boolean;      // Use virtual scrolling for long lists
  itemHeight?: number;          // Fixed item height (for virtual scroll)
  
  // Debouncing
  debounce?: number;            // Debounce onChange events (ms)
  throttle?: number;            // Throttle onChange events (ms)
}
```

**Example: Large Select with Virtual Scrolling**
```yaml
field:
  name: product_id
  type: select
  optionsSource: /api/products  # 10,000 products
  virtualScroll: true
  itemHeight: 40
  searchable: true
  debounce: 300  # Debounce search input
```

## Error Handling Contract

Components must gracefully handle errors and edge cases.

### Error States

```typescript
interface ErrorHandling {
  // Validation errors
  error?: string;               // Error message
  errorPosition?: 'top' | 'bottom' | 'inline';
  
  // Loading states
  loading?: boolean;            // Show loading indicator
  loadingText?: string;         // Loading message
  
  // Empty states
  empty?: boolean;              // No data available
  emptyText?: string;           // Empty state message
  emptyAction?: Action;         // Call-to-action when empty
  
  // Error recovery
  retry?: () => void;           // Retry failed action
  fallback?: any;               // Fallback value on error
}
```

**Example: Lookup with Error Handling**
```yaml
field:
  name: manager_id
  type: lookup
  object: user
  displayField: name
  optionsSource: /api/users/managers
  loading: true
  loadingText: Loading managers...
  emptyText: No managers found
  emptyAction:
    type: standard_new
    label: Create Manager
  retry: true  # Show retry button on fetch error
```

## Component Lifecycle

Components go through a standard lifecycle.

### Lifecycle Phases

```
1. Initialize
   ↓
2. Mount (render for first time)
   ↓
3. Update (props/state change)
   ↓
4. Validate (on blur or submit)
   ↓
5. Unmount (cleanup)
```

### Lifecycle Hooks

```typescript
interface LifecycleHooks {
  onInit?: () => void;          // Before first render
  onMount?: () => void;         // After first render
  onUpdate?: (prevProps: Props) => void;  // After props change
  onValidate?: () => ValidationResult;    // On validation
  onUnmount?: () => void;       // Before component removed
}
```

**Example:**
```yaml
field:
  name: email
  type: email
  lifecycle:
    onMount:
      - action: fetch_user_email
        endpoint: /api/user/email
    onBlur:
      - action: validate_email
      - action: check_duplicates
    onUnmount:
      - action: save_draft
```

## Widget Types Reference

### Input Widgets

| Type | Props | Events | Example |
|------|-------|--------|---------|
| `text` | maxLength, pattern | onChange, onBlur | Name, Title |
| `textarea` | rows, maxLength | onChange, onInput | Description |
| `email` | pattern | onChange, onBlur | Email Address |
| `tel` | pattern | onChange | Phone Number |
| `url` | pattern | onChange | Website |
| `password` | minLength | onChange | Password |

### Selection Widgets

| Type | Props | Events | Example |
|------|-------|--------|---------|
| `select` | options, multiple | onChange | Country, Status |
| `radio` | options | onChange | Gender, Size |
| `checkbox` | - | onChange | Accept Terms |
| `toggle` | trueLabel, falseLabel | onChange | Enable Feature |
| `lookup` | object, displayField | onChange, onSearch | Account, Contact |

### Date/Time Widgets

| Type | Props | Events | Example |
|------|-------|--------|---------|
| `date` | min, max | onChange | Birth Date |
| `datetime` | showTime | onChange | Appointment |
| `time` | format | onChange | Meeting Time |
| `daterange` | - | onChange | Start/End Date |

### Numeric Widgets

| Type | Props | Events | Example |
|------|-------|--------|---------|
| `number` | min, max, step | onChange | Quantity, Age |
| `currency` | currency, precision | onChange | Price, Revenue |
| `percent` | precision | onChange | Discount, Tax |
| `slider` | min, max, step | onChange | Volume, Rating |

### Rich Content Widgets

| Type | Props | Events | Example |
|------|-------|--------|---------|
| `richtext` | toolbar, plugins | onChange | Description |
| `markdown` | preview | onChange | Documentation |
| `code` | language, theme | onChange | API Key, JSON |
| `file` | accept, maxSize | onUpload | Attachment |

## Real-World Example: Complete Form Field

```yaml
field:
  # Identification
  name: customer_email
  type: email
  
  # Display
  label: Email Address
  placeholder: you@company.com
  helpText: We'll use this for account notifications
  icon: mail
  
  # Validation
  required: Email is required
  pattern: ^[^\s@]+@[^\s@]+\.[^\s@]+$
  patternMessage: Please enter a valid email address
  asyncValidator:
    endpoint: /api/customers/check-email
    debounce: 500
    errorMessage: This email is already registered
  
  # Layout
  span: 6
  
  # Behavior
  autocomplete: email
  spellcheck: false
  
  # Events
  onChange:
    - action: validate_email
  onBlur:
    - action: check_duplicates
    - action: format_email
      transform: toLowerCase
  
  # Accessibility
  ariaLabel: Customer email address
  tabIndex: 1
  
  # Performance
  debounce: 300
```

## What's Next?

<Cards>
  <Card
    icon={<Zap />}
    title="Action Protocol"
    href="/docs/protocols/objectui/actions"
    description="Define buttons, triggers, and navigation flows"
  />
  <Card
    icon={<Code />}
    title="Custom Components"
    href="/docs/guides/custom-components"
    description="Build and register your own widgets"
  />
</Cards>

## Related Resources

- [FormView Reference](/docs/references/ui/FormView) - Form configuration API
- [Field Types Reference](/docs/references/ui/fields) - Complete field type catalog
- [Validation Guide](/docs/guides/validation) - Validation patterns and examples
- [Accessibility Guide](/docs/guides/accessibility) - WCAG compliance checklist
