HotCRM Logo

FormView Layout Types

Guide to all 6 FormView layout types available in HotCRM — simple, tabbed, wizard, modal, drawer, and split.

FormView Layout Types

FormViews define how data entry forms are rendered for each business object. Every *.form.ts file exports a FormView validated against FormViewSchema from @objectstack/spec/ui. This guide covers all six layout types with real examples from HotCRM packages.

File convention: packages/{package}/src/{object}.form.ts — or {object}_{variant}.form.ts for alternate layouts.

Schema Overview

Every FormView follows the same top-level structure:

import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';

export const MyForm = {
  type: 'simple' | 'tabbed' | 'wizard' | 'modal' | 'drawer' | 'split',
  data: {
    provider: 'object',
    object: 'my_object'       // snake_case object name
  },
  sections: [ /* ... */ ]
} satisfies FormView;

FormViewSchema.parse(MyForm);
export default MyForm;

Simple

A standard single-page form with one or more sections stacked vertically. This is the default layout and the most commonly used.

When to use: Full create/edit forms where all fields are visible on one page.

// packages/crm/src/lead.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';

export const LeadForm = {
  type: 'simple' as const,
  data: {
    provider: 'object' as const,
    object: 'lead'
  },
  sections: [
    {
      label: 'Lead Information',
      columns: '2' as const,
      fields: [
        { field: 'first_name', required: true, placeholder: 'First name' },
        { field: 'last_name', required: true, placeholder: 'Last name' },
        { field: 'company', required: true, placeholder: 'Company or organization' },
        { field: 'title', placeholder: 'Job title' },
        { field: 'email', required: true, placeholder: 'email@example.com' },
        { field: 'phone', placeholder: '+1 (555) 000-0000' },
        { field: 'mobile_phone' },
        { field: 'website', placeholder: 'https://', visibleOn: "company != ''" },
        { field: 'industry', dependsOn: 'company' },
        { field: 'lead_source' },
        { field: 'description', colSpan: 2, widget: 'richtext' }
      ]
    },
    {
      label: 'Address',
      columns: '2' as const,
      collapsible: true,
      collapsed: true,
      fields: [
        { field: 'street', colSpan: 2 },
        { field: 'city' },
        { field: 'state', dependsOn: 'country' },
        { field: 'postal_code' },
        { field: 'country' }
      ]
    }
  ]
} satisfies FormView;

FormViewSchema.parse(LeadForm);
export default LeadForm;

Tabbed

Sections are rendered as horizontal tabs. Each section becomes its own tab pane, keeping long forms organized without vertical scrolling.

When to use: Records with many fields that fall into distinct categories (e.g., details, line items, payment terms).

// packages/finance/src/invoice.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';

export const InvoiceForm = {
  type: 'tabbed' as const,
  data: {
    provider: 'object' as const,
    object: 'invoice'
  },
  sections: [
    {
      label: 'Invoice Details',
      columns: '2' as const,
      fields: [
        { field: 'invoice_number', readonly: true, helpText: 'Auto-generated' },
        { field: 'account_id', label: 'Account', required: true },
        { field: 'contact_id', label: 'Contact' },
        { field: 'invoice_date', required: true },
        { field: 'due_date', required: true },
        { field: 'status' },
        { field: 'currency' }
      ]
    },
    {
      label: 'Line Items',
      columns: '1' as const,
      fields: [
        { field: 'description', widget: 'richtext' },
        { field: 'subtotal', readonly: true },
        { field: 'tax_amount', readonly: true },
        { field: 'total_amount', readonly: true, helpText: 'Calculated from line items' }
      ]
    },
    {
      label: 'Payment Terms',
      columns: '2' as const,
      fields: [
        { field: 'payment_terms' },
        { field: 'payment_method' },
        { field: 'billing_street' },
        { field: 'billing_city' },
        { field: 'billing_state' },
        { field: 'billing_postal_code' },
        { field: 'billing_country' }
      ]
    }
  ]
} satisfies FormView;

FormViewSchema.parse(InvoiceForm);
export default InvoiceForm;

Wizard

A multi-step form where each section is a sequential step. Users progress through steps with Next/Back navigation. Ideal for guided data entry workflows.

When to use: Onboarding flows, multi-step processes, or forms where later steps depend on earlier input.

// packages/hr/src/employee_onboarding.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';

export const EmployeeOnboardingForm = {
  type: 'wizard' as const,
  data: {
    provider: 'object' as const,
    object: 'employee'
  },
  sections: [
    {
      label: 'Personal Information',
      columns: '2' as const,
      fields: [
        { field: 'first_name', required: true },
        { field: 'last_name', required: true },
        { field: 'email', required: true, placeholder: 'work@company.com' },
        { field: 'personal_email' },
        { field: 'phone' },
        { field: 'date_of_birth' },
        { field: 'gender' }
      ]
    },
    {
      label: 'Role & Department',
      columns: '2' as const,
      fields: [
        { field: 'department_id', label: 'Department', required: true },
        { field: 'position_id', label: 'Position', required: true },
        { field: 'manager_id', label: 'Manager' },
        { field: 'hire_date', required: true },
        { field: 'employment_type' },
        { field: 'work_location' }
      ]
    },
    {
      label: 'IT Setup',
      columns: '2' as const,
      fields: [
        { field: 'employee_number', readonly: true, helpText: 'Auto-generated after provisioning' },
        { field: 'work_location' }
      ]
    },
    {
      label: 'Review & Confirm',
      columns: '1' as const,
      fields: [
        { field: 'first_name', readonly: true },
        { field: 'last_name', readonly: true },
        { field: 'email', readonly: true },
        { field: 'department_id', readonly: true, label: 'Department' },
        { field: 'position_id', readonly: true, label: 'Position' },
        { field: 'hire_date', readonly: true }
      ]
    }
  ]
} satisfies FormView;

FormViewSchema.parse(EmployeeOnboardingForm);
export default EmployeeOnboardingForm;

A compact overlay dialog for quick record creation. Only essential fields are shown so users can create records without leaving their current context.

When to use: Quick-create actions, inline record creation from related lists, or any form that should not navigate away from the current page.

// packages/crm/src/lead_quick_create.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';

export const LeadQuickCreateForm = {
  type: 'modal' as const,
  data: {
    provider: 'object' as const,
    object: 'lead'
  },
  sections: [
    {
      label: 'Lead Information',
      columns: '2' as const,
      fields: [
        { field: 'first_name', required: true },
        { field: 'last_name', required: true },
        { field: 'company' },
        { field: 'email', required: true, placeholder: 'name@company.com' },
        { field: 'phone', placeholder: '+1 (555) 000-0000' },
        { field: 'lead_source' }
      ]
    }
  ]
} satisfies FormView;

FormViewSchema.parse(LeadQuickCreateForm);
export default LeadQuickCreateForm;

Drawer

A slide-in side panel for quick record creation. Similar to modal but anchored to the edge of the viewport, keeping the underlying page partially visible.

When to use: Quick-create from a list view or record page where you want context from the parent page to remain visible.

// packages/crm/src/opportunity_quick_create.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';

export const OpportunityQuickCreateForm = {
  type: 'drawer' as const,
  data: {
    provider: 'object' as const,
    object: 'opportunity'
  },
  sections: [
    {
      label: 'Deal Information',
      columns: '2' as const,
      fields: [
        { field: 'name', required: true, placeholder: 'Enter deal name' },
        { field: 'account_id', label: 'Account' },
        { field: 'amount' },
        { field: 'close_date', required: true },
        { field: 'stage', required: true }
      ]
    }
  ]
} satisfies FormView;

FormViewSchema.parse(OpportunityQuickCreateForm);
export default OpportunityQuickCreateForm;

Split

A side-by-side layout where sections are displayed in two equal panels. Each section occupies one panel, making it easy to compare or group related information.

When to use: Records that have two natural groupings (e.g., account info vs. address, billing vs. shipping).

// packages/crm/src/account_split.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';

export const AccountSplitForm = {
  type: 'split' as const,
  data: {
    provider: 'object' as const,
    object: 'account'
  },
  sections: [
    {
      label: 'Account Information',
      columns: '1' as const,
      fields: [
        { field: 'name', required: true },
        { field: 'type' },
        { field: 'industry' },
        { field: 'phone' },
        { field: 'website' },
        { field: 'annual_revenue' },
        { field: 'ownership' }
      ]
    },
    {
      label: 'Billing & Shipping',
      columns: '1' as const,
      fields: [
        { field: 'billing_street' },
        { field: 'billing_city' },
        { field: 'billing_state' },
        { field: 'billing_postal_code' },
        { field: 'billing_country' },
        { field: 'shipping_street' },
        { field: 'shipping_city' },
        { field: 'shipping_state' },
        { field: 'shipping_postal_code' },
        { field: 'shipping_country' }
      ]
    }
  ]
} satisfies FormView;

FormViewSchema.parse(AccountSplitForm);
export default AccountSplitForm;

Layout Type Summary

TypeSections Rendered AsBest For
simpleStacked verticallyStandard create/edit forms
tabbedHorizontal tab panesMulti-category records
wizardSequential stepsGuided workflows, onboarding
modalOverlay dialogQuick-create without navigation
drawerSlide-in side panelQuick-create with context
splitSide-by-side panelsComparing or grouping data

Field-Level Features

Every field entry in a section supports the following properties:

PropertyTypeDescription
fieldstringRequired. The API name of the field on the object.
labelstringOverride the default field label.
requiredbooleanMark the field as required for form submission.
readonlybooleanRender the field as read-only (e.g., auto-generated values).
hiddenbooleanHide the field from the form while still including it in the data model.
helpTextstringInline help text displayed below the field (e.g., 'Auto-generated').
placeholderstringPlaceholder text shown inside the input when empty.
widgetstringOverride the default widget (e.g., 'richtext' for a rich text editor).
visibleOnstringExpression that controls field visibility (e.g., "company != ''").
dependsOnstringField that must have a value before this field becomes active.
colSpannumberNumber of grid columns this field spans (e.g., 2 for full-width).

Example: Field Features in Action

fields: [
  { field: 'invoice_number', readonly: true, helpText: 'Auto-generated' },
  { field: 'website', placeholder: 'https://', visibleOn: "company != ''" },
  { field: 'state', dependsOn: 'country' },
  { field: 'description', colSpan: 2, widget: 'richtext' },
  { field: 'email', required: true, placeholder: 'email@example.com' }
]

Section Features

Each section in the sections array supports:

PropertyTypeDescription
labelstringRequired. The section heading (also used as the tab/step title).
columns'1' | '2'Number of columns for the field grid layout.
collapsiblebooleanAllow the section to be collapsed by the user.
collapsedbooleanStart the section in a collapsed state (requires collapsible: true).
fieldsarrayArray of field definitions for this section.

Example: Collapsible Section

{
  label: 'Address',
  columns: '2' as const,
  collapsible: true,
  collapsed: true,
  fields: [
    { field: 'street', colSpan: 2 },
    { field: 'city' },
    { field: 'state', dependsOn: 'country' },
    { field: 'postal_code' },
    { field: 'country' }
  ]
}

Quick Reference

What you wantHow to do it
Full-width fieldcolSpan: 2 in a 2-column section
Conditional visibilityvisibleOn: "field_name != ''"
Dependent dropdowndependsOn: 'parent_field'
Rich text editorwidget: 'richtext'
Auto-generated fieldreadonly: true + helpText: 'Auto-generated'
Collapsed by defaultcollapsible: true + collapsed: true on the section
Quick-create dialogtype: 'modal' with minimal fields
Side panel formtype: 'drawer'
Step-by-step flowtype: 'wizard' with one section per step

On this page